const resourceName = typeof GetParentResourceName === 'function' ? GetParentResourceName() : 'bleeter';
const state = {
visible: false,
mode: 'phone', // 'phone' | 'desktop'
page: 'feed',
data: null,
fallback: true,
likedPosts: new Set([201]),
commentsOpen: new Set(),
searchQuery: '',
selectedCalendarDay: 3,
marketQuery: '',
marketSort: 'newest',
likeDeltas: {},
postComments: {},
likedComments: new Set(),
commentLikeDeltas: {},
editingProfile: false,
pendingUploadTarget: null,
composeMedia: {},
viewedProfileHandle: null
};
const fallbackData = {
account: {
displayName: 'Zoey Reed',
mailAddress: 'zoeyreed@mail.ls'
},
navigation: [],
profiles: [
{
id: 1,
profile_type: 'private',
handle: 'zoeyreed',
display_name: 'Zoey Reed',
avatar_url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=256&h=256&fit=crop',
banner_url: 'https://images.unsplash.com/photo-1519608487953-e999c86e7455?w=1200&h=320&fit=crop',
bio: 'Los Santos, Kaffee, Geschichten und viel zu viele Benachrichtigungen.',
is_verified: false
},
{
id: 2,
profile_type: 'small_business',
handle: 'reed.events',
display_name: 'Reed Events',
avatar_url: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=256&h=256&fit=crop',
banner_url: 'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=1200&h=320&fit=crop',
bio: 'Kleine Events, grosse Abende.',
is_verified: true
}
],
activeProfileId: 1,
profileDirectory: [],
posts: [
{
id: 101,
feed_type: 'home',
author: {
handle: 'lasky',
display_name: 'A.J. Lasky III',
avatar_url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=256&h=256&fit=crop',
is_verified: false
},
body: '"Er ist ein stiller Waechter .... ein wachsamer Beschuetzer. Ein dunkler Ritter" - Jim Gordon',
media_url: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1100&h=680&fit=crop',
created_at: 'Heute, 22:15',
likes: 12,
comments: 3
},
{
id: 102,
feed_type: 'home',
author: {
handle: 'm4ttr1x',
display_name: '@m4ttr1x',
avatar_url: 'https://images.unsplash.com/photo-1518709268805-4e9042af2176?w=256&h=256&fit=crop',
is_verified: false
},
body: 'Wir haben zugesehen. ShitInvaders Sicherheit? Laecherlich. Ihr System ist genauso kaputt wie ihre Versprechen.\n\nDas hier ist euer Weckruf.',
created_at: '28.12.2024, 22:21:53',
likes: 1,
comments: 0
},
{
id: 201,
feed_type: 'advertising',
author: {
handle: 'lsmd',
display_name: 'Los Santos Medical Department',
avatar_url: 'https://images.unsplash.com/photo-1584515933487-779824d29309?w=256&h=256&fit=crop',
is_verified: true
},
body: 'Heute Blutspendeaktion am Pillbox Medical Center. Kommt vorbei, Los Santos.',
created_at: 'Heute, 19:40',
likes: 44,
comments: 8
}
],
businesses: [
{
handle: 'dmv',
display_name: 'Department of Motorvehicles',
avatar_url: 'https://images.unsplash.com/photo-1511919884226-fd3cad34687c?w=256&h=256&fit=crop',
banner_url: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=1000&h=260&fit=crop',
status: 'open',
is_verified: true
},
{
handle: 'doj',
display_name: 'Department of Justice',
avatar_url: 'https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=256&h=256&fit=crop',
banner_url: 'https://images.unsplash.com/photo-1453945619913-79ec89a82c51?w=1000&h=260&fit=crop',
status: 'open',
is_verified: true
},
{
handle: 'gruppe6',
display_name: 'Gruppe 6',
avatar_url: 'https://images.unsplash.com/photo-1517048676732-d65bc937f952?w=256&h=256&fit=crop',
banner_url: 'https://images.unsplash.com/photo-1521791136064-7986c2920216?w=1000&h=260&fit=crop',
status: 'closed',
is_verified: false
},
{
handle: 'herrkutz',
display_name: 'Herr Kutz',
avatar_url: 'https://images.unsplash.com/photo-1503951914875-452162b0f3f1?w=256&h=256&fit=crop',
banner_url: 'https://images.unsplash.com/photo-1585747860715-2ba37e788b70?w=1000&h=260&fit=crop',
status: 'closed',
is_verified: true
}
],
suggestions: [
{ handle: 'lsmd', display_name: 'Los Santos Medical Department' },
{ handle: 'dynasty8', display_name: 'Dynasty 8' },
{ handle: 'usms', display_name: 'US Marshal Service' }
],
trends: [
{ tag: '#gruppe6', posts: 77 },
{ tag: '#lalintera', posts: 43 },
{ tag: '#JoinTheTeam', posts: 39 },
{ tag: '#LSFD', posts: 39 }
],
users: [
{
handle: 'zoeyreed',
display_name: 'Zoey Reed',
avatar_url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=256&h=256&fit=crop',
profile_type: 'private',
can_be_blocked: true
},
{
handle: 'zoey.reed',
display_name: 'Zoey Reed',
avatar_url: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=256&h=256&fit=crop',
profile_type: 'private',
can_be_blocked: true
},
{
handle: 'z_reed',
display_name: 'Z. Reed',
avatar_url: 'https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?w=256&h=256&fit=crop',
profile_type: 'private',
can_be_blocked: true
},
{
handle: 'lasky',
display_name: 'A.J. Lasky III',
avatar_url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=256&h=256&fit=crop',
profile_type: 'private',
can_be_blocked: true
}
],
social: {
following: [
{ handle: 'lasky', display_name: 'A.J. Lasky III', avatar_url: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=256&h=256&fit=crop', profile_type: 'private', can_be_blocked: true }
],
followers: [],
blocked: []
},
marketplace: [
{ id: 1, title: 'BMX in gutem Zustand', body: 'Schwarzes BMX, kaum gefahren. Abholung in Vespucci.', price: 450, price_label: '$450', created: 4, can_delete: true, author: { handle: 'zoeyreed', display_name: 'Zoey Reed', avatar_url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=256&h=256&fit=crop', email: 'zoeyreed@mail.ls', is_verified: false } },
{ id: 2, title: 'Catering fuer kleine Feiern', body: 'Fingerfood und Getraenke fuer Events bis 25 Personen.', price: 1200, price_label: '$1200', created: 2, can_delete: false, author: { handle: 'reed.events', display_name: 'Reed Events', avatar_url: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?w=256&h=256&fit=crop', email: 'reed.events@mail.ls', is_verified: true } },
{ id: 3, title: 'Reinigung nach Clubabend', body: 'Diskret, schnell, mit eigenem Equipment.', price: 800, price_label: '$800', created: 1, can_delete: false, author: { handle: 'cleanls', display_name: 'Clean LS', avatar_url: '', email: 'cleanls@mail.ls', is_verified: false } }
],
calendarDays: [
{ offset: 0, date: '2026-05-19', title: 'Dienstag 19.05.2026', events: [{ id: 1, time: '18:00', title: 'LSPD Coffe with Cops', location: '', author: 'CoolBeans', can_delete: false }] },
{ offset: 1, date: '2026-05-20', title: 'Mittwoch 20.05.2026', events: [] },
{ offset: 2, date: '2026-05-21', title: 'Donnerstag 21.05.2026', events: [] },
{ offset: 3, date: '2026-05-22', title: 'Freitag 22.05.2026', events: [{ id: 2, time: '20:00', title: 'Happy Hour', location: 'Craftbar', author: 'Craftbar', can_delete: false }, { id: 3, time: '21:00', title: 'All you can eat', location: '', author: 'Burgershot', can_delete: false }] },
{ offset: 4, date: '2026-05-23', title: 'Samstag 23.05.2026', events: [{ id: 4, time: '21:00', title: 'Geocaching Legionsquar', location: '', author: 'Lifeinvader', can_delete: false }] },
{ offset: 5, date: '2026-05-24', title: 'Sonntag 24.05.2026', events: [] },
{ offset: 6, date: '2026-05-25', title: 'Montag 25.05.2026', events: [] },
{ offset: 7, date: '2026-05-26', title: 'Dienstag 26.05.2026', events: [] },
{ offset: 8, date: '2026-05-27', title: 'Mittwoch 27.05.2026', events: [] },
{ offset: 9, date: '2026-05-28', title: 'Donnerstag 28.05.2026', events: [] },
{ offset: 10, date: '2026-05-29', title: 'Freitag 29.05.2026', events: [] },
{ offset: 11, date: '2026-05-30', title: 'Samstag 30.05.2026', events: [] },
{ offset: 12, date: '2026-05-31', title: 'Sonntag 31.05.2026', events: [] },
{ offset: 13, date: '2026-06-01', title: 'Montag 01.06.2026', events: [] },
{ offset: 14, date: '2026-06-02', title: 'Dienstag 02.06.2026', events: [] },
{ offset: 15, date: '2026-06-03', title: 'Mittwoch 03.06.2026', events: [] },
{ offset: 16, date: '2026-06-04', title: 'Donnerstag 04.06.2026', events: [] },
{ offset: 17, date: '2026-06-05', title: 'Freitag 05.06.2026', events: [] },
{ offset: 18, date: '2026-06-06', title: 'Samstag 06.06.2026', events: [] },
{ offset: 19, date: '2026-06-07', title: 'Sonntag 07.06.2026', events: [] },
{ offset: 20, date: '2026-06-08', title: 'Montag 08.06.2026', events: [] },
{ offset: 21, date: '2026-06-09', title: 'Dienstag 09.06.2026', events: [] },
{ offset: 22, date: '2026-06-10', title: 'Mittwoch 10.06.2026', events: [] },
{ offset: 23, date: '2026-06-11', title: 'Donnerstag 11.06.2026', events: [] },
{ offset: 24, date: '2026-06-12', title: 'Freitag 12.06.2026', events: [] },
{ offset: 25, date: '2026-06-13', title: 'Samstag 13.06.2026', events: [] },
{ offset: 26, date: '2026-06-14', title: 'Sonntag 14.06.2026', events: [] },
{ offset: 27, date: '2026-06-15', title: 'Montag 15.06.2026', events: [] }
],
config: {
navigation: [
{ key: 'feed', label: 'Feed', icon: 'H' },
{ key: 'ads', label: 'Werbefeed', icon: 'W' },
{ key: 'followers', label: 'Follower', icon: 'F' },
{ key: 'market', label: 'Marktplatz', icon: 'M' },
{ key: 'calendar', label: 'Kalender', icon: 'K' },
{ key: 'business', label: 'Gewerbe', icon: 'G' },
{ key: 'profile', label: 'Profil', icon: 'P' },
{ key: 'legal', label: 'Rechtliches', icon: 'R' }
]
}
};
const root = document.getElementById('bleeter-root');
const nav = document.getElementById('side-nav');
const content = document.getElementById('content');
const context = document.getElementById('context');
const topProfileAvatar = document.getElementById('top-profile-avatar');
const globalSearch = document.getElementById('global-search');
const searchResults = document.getElementById('search-results');
const mediaFileInput = document.getElementById('media-file-input');
function post(name, payload = {}) {
return fetch(`https://${resourceName}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(payload)
}).catch(() => {});
}
function data() {
return state.data || fallbackData;
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, char => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[char]));
}
function activeProfile() {
const current = data();
return (current.profiles || []).find(profile => profile.id === current.activeProfileId) || current.profiles?.[0] || {};
}
function allKnownProfiles() {
const current = data();
const byHandle = new Map();
const addProfile = item => {
if (!item?.handle) return;
byHandle.set(item.handle, { ...(byHandle.get(item.handle) || {}), ...item });
};
(current.profileDirectory || []).forEach(addProfile);
(current.profiles || []).forEach(addProfile);
(current.users || []).forEach(addProfile);
(current.businesses || []).forEach(addProfile);
(current.social?.following || []).forEach(addProfile);
(current.social?.followers || []).forEach(addProfile);
(current.social?.blocked || []).forEach(addProfile);
(current.posts || []).forEach(postItem => addProfile(postItem.author));
(current.marketplace || []).forEach(item => addProfile(item.author));
return [...byHandle.values()];
}
function findKnownProfile(handle) {
const cleanHandle = String(handle || '').replace(/^@/, '');
if (!cleanHandle) return null;
return allKnownProfiles().find(profile => profile.handle === cleanHandle) || null;
}
function viewedProfile() {
if (!state.viewedProfileHandle) return activeProfile();
return findKnownProfile(state.viewedProfileHandle) || activeProfile();
}
function isOwnProfile(profile) {
const active = activeProfile();
return Number(profile?.id) === Number(active.id) || profile?.handle === active.handle;
}
function openProfile(handle) {
const cleanHandle = String(handle || '').replace(/^@/, '');
state.viewedProfileHandle = cleanHandle || null;
state.editingProfile = false;
state.page = 'profile';
state.searchQuery = '';
render();
}
function isServerBacked() {
return state.data !== null;
}
function lifeinvaderPermissions() {
const permissions = data().permissions?.lifeinvader;
return typeof permissions === 'object' && permissions !== null ? permissions : {};
}
function hydrateInteractionState() {
state.likedPosts = new Set();
state.likedComments = new Set();
state.postComments = {};
state.likeDeltas = {};
state.commentLikeDeltas = {};
for (const postItem of state.data?.posts || []) {
if (postItem.liked_by_viewer) {
state.likedPosts.add(postItem.id);
}
if (Array.isArray(postItem.comments_list)) {
state.postComments[postItem.id] = postItem.comments_list;
for (const comment of postItem.comments_list) {
if (comment.liked_by_viewer) {
state.likedComments.add(String(comment.id));
}
}
}
}
}
function verified(profile) {
return profile?.is_verified ? `${patchCheckIcon()} ` : '';
}
function lifeinvaderStaffBadge(profile) {
return profile?.is_lifeinvader_staff ? `${awardIcon()} ` : '';
}
function profileBadges(profile) {
return `${verified(profile)}${lifeinvaderStaffBadge(profile)}`;
}
function patchCheckIcon() {
return `
`;
}
function awardIcon() {
return `
`;
}
function slashCircleIcon() {
return `
`;
}
function renderLifeinvaderProfileActions(profile) {
const permissions = lifeinvaderPermissions();
const buttons = [];
const handle = escapeHtml(profile.handle || '');
if (permissions.can_verify) {
buttons.push(`
${patchCheckIcon()}
`);
}
if (permissions.can_staff) {
buttons.push(`
${awardIcon()}
`);
}
if (permissions.can_lock) {
buttons.push(`
${slashCircleIcon()}
`);
}
return buttons.length ? `
${buttons.join('')}
` : '';
}
function trashIcon() {
return `
`;
}
function mailIcon() {
return `
`;
}
function profileStatIcon(type) {
const icons = {
posts: `
`,
followers: `
`,
following: `
`,
market: `
`,
calendar: `
`
};
return icons[type] || '';
}
function navIconGlyph(icon, label) {
return {
home: 'F',
megaphone: 'W',
users: 'Fo',
basket: 'M',
calendar: 'K',
briefcase: 'G',
user: 'P',
book: 'R'
}[icon] || String(label || '?').slice(0, 2);
}
function navIconMarkup(item) {
const icons = {
feed: `
`,
ads: `
`,
followers: `
`,
market: `
`,
calendar: `
`,
business: `
`,
profile: `
`,
legal: `
`
};
return icons[item.key] || escapeHtml(navIconGlyph(item.icon, item.label));
}
function show(open, mode) {
state.visible = open;
if (open && mode) state.mode = mode;
root.classList.toggle('hidden', !open);
root.classList.toggle('mode-desktop', open && state.mode === 'desktop');
root.classList.toggle('mode-phone', open && state.mode !== 'desktop');
if (open) {
post('requestBootstrap');
}
render();
}
function renderNav() {
const items = data().config?.navigation || fallbackData.config.navigation;
nav.innerHTML = items.map(item => `
${navIconMarkup(item)}
${escapeHtml(item.label)}
`).join('');
}
function renderTopbar() {
const profile = activeProfile();
topProfileAvatar.src = profile.avatar_url || '';
if (globalSearch.value !== state.searchQuery) {
globalSearch.value = state.searchQuery;
}
renderSearchResults();
}
function renderSearchResults() {
const query = state.searchQuery.trim().toLowerCase().replace(/^@/, '');
if (!query) {
searchResults.classList.add('hidden');
searchResults.innerHTML = '';
return;
}
const matches = (data().users || fallbackData.users).filter(user => user.handle.startsWith(query)).slice(0, 5);
searchResults.classList.toggle('hidden', matches.length === 0);
searchResults.innerHTML = matches.map(user => `
${escapeHtml(user.display_name)}
@${escapeHtml(user.handle)}
`).join('');
}
function renderCompose(feedType) {
const profile = activeProfile();
const canPost = (profile.profile_type === 'private' && feedType === 'home')
|| (['small_business', 'company'].includes(profile.profile_type) && feedType === 'advertising')
|| (profile.profile_type === 'authority');
if (!canPost) {
return `
Nur lesen
Dein aktives Profil darf hier liken und kommentieren, aber nicht posten.
`;
}
return `
`;
}
function renderPost(post) {
const author = post.author || {};
const liked = state.likedPosts.has(post.id);
const commentsOpen = state.commentsOpen.has(post.id);
const likeCount = Number(post.likes || 0) + Number(state.likeDeltas[post.id] || 0);
const comments = commentsForPost(post.id);
const commentCount = comments.length;
return `
${escapeHtml(author.display_name)} ${profileBadges(author)}
@${escapeHtml(author.handle)}
${escapeHtml(post.created_at)}
${post.body ? `${escapeHtml(post.body)}
` : ''}
${post.media_url ? ` ` : ''}
${liked ? '🤍' : '♡'} ${likeCount}
Kommentare ${commentCount}
${post.can_delete ? `${trashIcon()} ` : ''}
${commentsOpen ? renderComments(post, comments) : ''}
`;
}
function commentsForPost(postId) {
if (!state.postComments[postId]) {
const postItem = (data().posts || []).find(item => Number(item.id) === Number(postId));
state.postComments[postId] = Array.isArray(postItem?.comments_list)
? postItem.comments_list
: [
{ id: `${postId}-1`, author: 'zoeyreed', body: 'Guter Hinweis, danke fuer die Info.', likes: 0 },
{ id: `${postId}-2`, author: 'lasky', body: 'Bin dabei.', likes: 0 }
];
}
return state.postComments[postId];
}
function renderComments(post, comments) {
return `
`;
}
function renderComment(comment) {
const commentId = String(comment.id);
const liked = state.likedComments.has(commentId);
const likeCount = Number(comment.likes || 0) + Number(state.commentLikeDeltas[comment.id] || 0);
return `
`;
}
function renderFeed(feedType) {
const title = feedType === 'home' ? 'Feed' : 'Werbefeed';
const posts = (data().posts || []).filter(post => post.feed_type === feedType);
content.innerHTML = `
${renderCompose(feedType)}
${posts.map(renderPost).join('')}
`;
}
function renderContextFeed() {
const current = data();
context.innerHTML = `
Wem folgen?
${(current.suggestions || []).map(item => `
${escapeHtml(item.display_name.slice(0, 1))}
${escapeHtml(item.display_name)}
@${escapeHtml(item.handle)}
Folgen
`).join('')}
Trends
${(current.trends || []).map(item => `
${escapeHtml(item.tag)}
${Number(item.posts || 0)} Posts
`).join('')}
`;
}
function renderBusiness() {
const current = data();
const open = (current.businesses || []).filter(item => item.status === 'open');
const closed = (current.businesses || []).filter(item => item.status !== 'open');
const card = item => `
${escapeHtml(item.display_name)} ${profileBadges(item)}
@${escapeHtml(item.handle)}
`;
content.innerHTML = `
${open.map(card).join('')}
${closed.map(card).join('')}
`;
context.innerHTML = `
Status
${open.length} geoeffnet Aktive Leitstellen werden hervorgehoben.
${closed.length} geschlossen Geschlossene Gewerbe sind ausgegraut.
`;
}
function renderMarketplace() {
const items = filteredMarketplaceItems();
content.innerHTML = `
${renderMarketplaceCompose()}
Neuester Post
Aeltester Post
Preis aufsteigend
Preis absteigend
${items.map(renderMarketplaceItem).join('')}
`;
context.innerHTML = `
Sortierung
${items.length} Treffer
Suche nach Begriffen im Titel, Text oder Profilnamen.
`;
}
function filteredMarketplaceItems() {
const query = state.marketQuery.trim().toLowerCase();
let items = (data().marketplace || fallbackData.marketplace).filter(item => {
if (!query) return true;
return `${item.title} ${item.body} ${item.author?.handle || ''} ${item.author?.display_name || ''}`.toLowerCase().includes(query);
});
return [...items].sort((a, b) => {
if (state.marketSort === 'price_asc') return Number(a.price || 0) - Number(b.price || 0);
if (state.marketSort === 'price_desc') return Number(b.price || 0) - Number(a.price || 0);
if (state.marketSort === 'oldest') return Number(a.created || 0) - Number(b.created || 0);
return Number(b.created || 0) - Number(a.created || 0);
});
}
function refreshMarketplaceResults() {
const items = filteredMarketplaceItems();
const results = document.getElementById('market-results');
if (results) {
results.innerHTML = items.map(renderMarketplaceItem).join('');
}
const hitCount = document.getElementById('market-hit-count');
if (hitCount) {
hitCount.textContent = `${items.length} Treffer`;
}
}
function renderMarketplaceItem(item) {
const author = item.author || {};
const mailSubject = `Bleeter Marktplatz: ${item.title || 'Inserat'}`;
return `
${author.avatar_url ? ` ` : `${escapeHtml((author.display_name || author.handle || '?').slice(0, 1))} `}
${escapeHtml(author.display_name || author.handle || 'Unbekannt')} ${profileBadges(author)}
${author.email ? `${mailIcon()} ` : ''}
@${escapeHtml(author.handle || '')}
${escapeHtml(item.title)}
${escapeHtml(item.body)}
${item.media_url ? `
` : ''}
${escapeHtml(item.price_label || ('$' + Number(item.price || 0)))}
${item.can_delete ? `
${trashIcon()} ` : ''}
`;
}
function renderProfileEventItem(day, event) {
const location = event.location ? ` ..${escapeHtml(event.location)}..` : '';
return `
${escapeHtml(day.title)}
${escapeHtml(event.time || '')} ${escapeHtml(event.title || '')}${location}
@${escapeHtml(event.author || '')}
`;
}
function renderMarketplaceCompose() {
const profile = activeProfile();
return `
`;
}
function renderCalendar() {
const days = data().calendarDays || fallbackData.calendarDays;
const selected = days.find(day => day.offset === state.selectedCalendarDay) || days[0];
const profile = activeProfile();
const canCreateEvent = ['small_business', 'company', 'authority'].includes(profile.profile_type);
content.innerHTML = `
${days.map(day => `
${escapeHtml(day.title)}
${day.events.map(event => renderCalendarEventLine(event)).join('')}
`).join('')}
`;
context.innerHTML = `
Veranstaltung
${canCreateEvent ? renderEventEditor(selected, profile) : `
Nur lesen
Termine koennen von Unternehmen, Kleingewerben und Behoerden eingetragen werden.
`}
`;
}
function renderCalendarEventLine(event) {
if (typeof event === 'string') {
return `${escapeHtml(event)}
`;
}
const location = event.location ? ` ..${escapeHtml(event.location)}..` : '';
const line = `${escapeHtml(event.time || '')} ${escapeHtml(event.title || '')} ${location} @${escapeHtml(event.author || '')}`;
return `
${line}
${event.can_delete ? `${trashIcon()} ` : ''}
`;
}
function renderEventEditor(selected, profile) {
return `
`;
}
function renderLegal() {
content.innerHTML = `
Rechtliches & Nutzungsbedingungen
Bleeter powered by Lifeinvader™
Mit der Nutzung von Lifeinvader Bleeter erklärst du dich damit einverstanden, dass deine veröffentlichten Inhalte, Profildaten, Nachrichten sowie Interaktionen innerhalb der Plattform durch Lifeinvader gespeichert, verarbeitet und für interne Zwecke ausgewertet werden dürfen.
Lifeinvader behält sich das Recht vor, öffentlich gepostete Bilder, Beiträge, Kommentare oder Profilelemente für Werbezwecke, Kampagnen, Empfehlungen oder Plattformdarstellungen zu verwenden. Mit dem Hochladen von Inhalten erteilst du Lifeinvader hierfür eine uneingeschränkte Nutzungserlaubnis innerhalb der Plattformdienste.
Zur Sicherheit und zum Schutz aller Nutzer gelten auf Bleeter folgende Grundregeln:
Keine Darstellung extremer Gewalt
Keine Veröffentlichung nackter oder eindeutig sexueller Inhalte
Keine gezielten Drohungen, Erpressung oder Aufrufe zu Straftaten
Keine Veröffentlichung vertraulicher personenbezogener Informationen anderer Bürger ohne deren Zustimmung
Lifeinvader behält sich jederzeit das Recht vor:
Beiträge oder Medien ohne Vorwarnung zu entfernen
Nutzerprofile temporär oder dauerhaft zu sperren
Reichweiten einzuschränken oder Inhalte unsichtbar zu schalten
Accounts bei Missbrauch oder wiederholten Verstößen dauerhaft zu deaktivieren
Beiträge, die gegen geltendes Recht oder die Plattformrichtlinien verstoßen, können zur weiteren Prüfung an das Department of Justice DOJ übermittelt werden. Dies betrifft insbesondere Inhalte mit Gewaltandrohungen, organisierter Kriminalität, Erpressung, Betrug oder der öffentlichen Verbreitung illegaler Aktivitäten.
Mit der Nutzung von Bleeter bestätigst du außerdem, dass du verstehst, dass Inhalte im Internet niemals vollständig privat sind. Denk nach, bevor du postest — das Netz vergisst nicht.
Lifeinvader™ — Wir wissen, was dich bewegt.
`;
context.innerHTML = '';
}
function renderProfile() {
const profile = viewedProfile();
const ownProfile = isOwnProfile(profile);
const social = data().social || fallbackData.social;
const following = socialHandles(social.following).has(profile.handle);
const blocked = socialHandles(social.blocked).has(profile.handle);
const visiblePosts = (data().posts || []).filter(post => post.author?.handle === profile.handle);
const visibleMarketplace = (data().marketplace || []).filter(item => item.author?.handle === profile.handle);
const visibleEvents = (data().calendarDays || [])
.flatMap(day => (day.events || [])
.filter(event => event.author === profile.handle)
.map(event => ({ day, event })));
const followersFallback = (social.followers || []).filter(item => item.handle === profile.handle).length;
const followingFallback = ownProfile ? (social.following || []).length : 0;
const followersCount = Number(profile.followers_count ?? followersFallback);
const followingCount = Number(profile.following_count ?? followingFallback);
const canBlock = profile.can_be_blocked !== false;
const avatarUrl = profile.avatar_url || '';
const bannerUrl = profile.banner_url || profile.avatar_url || '';
const contactMail = profile.email_contact || profile.email || '';
const moderationActions = renderLifeinvaderProfileActions(profile);
if (ownProfile && state.editingProfile && profile.can_edit_profile !== false) {
content.innerHTML = renderProfileEditor(profile);
} else {
const profileActions = ownProfile
? `
${profile.can_edit_profile !== false ? `Bearbeiten ` : ''}
${moderationActions}
`
: `
${following
? `Entfolgen `
: `Folgen `}
${contactMail ? `${mailIcon()} ` : ''}
${blocked
? `Entblocken `
: (canBlock
? `Blockieren `
: `Geschuetzt `)}
${moderationActions}
`;
content.innerHTML = `
${bannerUrl ? ` ` : '
'}
Profilinhalte
${ownProfile ? 'Deine sichtbaren Inhalte.' : `Sichtbare Inhalte von @${escapeHtml(profile.handle)}.`}
Posts
${visiblePosts.length ? visiblePosts.map(renderPost).join('') : '
Noch keine sichtbaren Posts.
'}
${visibleMarketplace.length ? `
Marktplatz
${visibleMarketplace.map(renderMarketplaceItem).join('')}
` : ''}
${visibleEvents.length ? `
Kalender
${visibleEvents.map(item => renderProfileEventItem(item.day, item.event)).join('')}
` : ''}
`;
}
if (ownProfile) {
context.innerHTML = `
Profil wechseln
${(data().profiles || []).map(item => `
${escapeHtml(item.display_name)} ${profileBadges(item)}
@${escapeHtml(item.handle)}
`).join('')}
`;
} else {
context.innerHTML = `
Profil
Zurueck zu mir
@${escapeHtml(activeProfile().handle || '')}
`;
}
}
function renderProfileEditor(profile) {
const bioMax = profile.profile_type === 'private' ? 200 : 500;
return `
`;
}
function renderSimplePage(title, subtitle, cards, contextTitle = '') {
content.innerHTML = `
${escapeHtml(title)}
${escapeHtml(subtitle)}
${cards.map(card => `
${escapeHtml(card.title)}
${escapeHtml(card.text)}
`).join('')}
`;
context.innerHTML = contextTitle ? `
${escapeHtml(contextTitle)}
V1 Diese Ansicht ist im Grundgeruest vorbereitet und bekommt als naechstes echte Daten.
` : '';
}
function socialHandles(list) {
return new Set((list || []).map(item => item.handle));
}
function renderSocialRow(item, mode, followingSet, blockedSet) {
const handle = item.handle || '';
const isFollowing = followingSet.has(handle);
const isBlocked = blockedSet.has(handle);
const canBlock = item.can_be_blocked !== false;
const blockAction = isBlocked
? `Entblocken `
: (canBlock
? `Blockieren `
: `Geschuetzt `);
const followAction = mode === 'blocked'
? ''
: (isFollowing
? `Entfolgen `
: `Folgen `);
return `
${item.avatar_url ? `
` : `
${escapeHtml((item.display_name || handle || '?').slice(0, 1))} `}
${escapeHtml(item.display_name || handle)} ${profileBadges(item)}
@${escapeHtml(handle)}
${followAction}${blockAction}
`;
}
function renderSocialSection(title, items, mode, followingSet, blockedSet, emptyText) {
const rows = (items || []).map(item => renderSocialRow(item, mode, followingSet, blockedSet)).join('');
return `
`;
}
function renderFollowers() {
const current = data();
const social = current.social || fallbackData.social;
const followingSet = socialHandles(social.following);
const blockedSet = socialHandles(social.blocked);
const suggestions = (current.users || fallbackData.users).filter(item => !blockedSet.has(item.handle)).slice(0, 12);
content.innerHTML = `
${renderSocialSection('Entdecken', suggestions, 'discover', followingSet, blockedSet, 'Keine Profile gefunden.')}
${renderSocialSection('Wem folge ich', social.following, 'following', followingSet, blockedSet, 'Du folgst aktuell niemandem.')}
${renderSocialSection('Wer folgt mir', social.followers, 'followers', followingSet, blockedSet, 'Noch keine Follower.')}
${renderSocialSection('Blockiert', social.blocked, 'blocked', followingSet, blockedSet, 'Keine blockierten Profile.')}
`;
context.innerHTML = '';
}
function renderPage() {
if (state.page === 'feed') {
renderFeed('home');
renderContextFeed();
return;
}
if (state.page === 'ads') {
renderFeed('advertising');
renderContextFeed();
return;
}
if (state.page === 'business') {
renderBusiness();
return;
}
if (state.page === 'profile') {
renderProfile();
return;
}
if (state.page === 'followers') {
renderFollowers();
return;
}
if (state.page === 'market') {
renderMarketplace();
return;
}
if (state.page === 'calendar') {
renderCalendar();
return;
}
if (state.page === 'legal') {
renderLegal();
}
}
function render() {
renderNav();
renderTopbar();
renderPage();
}
document.addEventListener('click', event => {
const pageButton = event.target.closest('[data-page]');
if (pageButton) {
state.page = pageButton.dataset.page;
if (state.page === 'profile') state.viewedProfileHandle = null;
if (state.page !== 'profile') state.editingProfile = false;
render();
return;
}
const openOwnProfileButton = event.target.closest('[data-open-own-profile]');
if (openOwnProfileButton) {
state.viewedProfileHandle = null;
state.page = 'profile';
state.editingProfile = false;
render();
return;
}
const openProfileButton = event.target.closest('[data-open-profile]');
if (openProfileButton) {
openProfile(openProfileButton.dataset.openProfile);
return;
}
const mailButton = event.target.closest('[data-mail-to]');
if (mailButton) {
post('openMailTo', {
mail: mailButton.dataset.mailTo,
subject: mailButton.dataset.mailSubject || ''
});
return;
}
const moderateProfileButton = event.target.closest('[data-moderate-profile]');
if (moderateProfileButton) {
const action = moderateProfileButton.dataset.moderateProfile;
const handle = moderateProfileButton.dataset.moderateProfileHandle;
if (!isServerBacked()) {
const profile = findKnownProfile(handle);
if (profile) {
if (action === 'verify') profile.is_verified = !profile.is_verified;
if (action === 'staff') profile.is_lifeinvader_staff = !profile.is_lifeinvader_staff;
if (action === 'lock') profile.is_locked = !profile.is_locked;
}
}
post('moderateProfile', { action, handle });
render();
return;
}
const profileJumpButton = event.target.closest('[data-profile-jump]');
if (profileJumpButton) {
const target = document.querySelector(`[data-profile-section="${profileJumpButton.dataset.profileJump}"]`);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
return;
}
const profileSocialButton = event.target.closest('[data-profile-social]');
if (profileSocialButton) {
state.page = 'followers';
state.editingProfile = false;
render();
return;
}
const followButton = event.target.closest('[data-follow-profile]');
if (followButton) {
const handle = followButton.dataset.followProfile;
if (!isServerBacked()) {
const item = fallbackData.users.find(user => user.handle === handle);
if (item && !fallbackData.social.following.some(user => user.handle === handle)) {
fallbackData.social.following.push(item);
}
}
post('followProfile', { handle });
render();
return;
}
const unfollowButton = event.target.closest('[data-unfollow-profile]');
if (unfollowButton) {
const handle = unfollowButton.dataset.unfollowProfile;
if (!isServerBacked()) {
fallbackData.social.following = fallbackData.social.following.filter(user => user.handle !== handle);
}
post('unfollowProfile', { handle });
render();
return;
}
const blockButton = event.target.closest('[data-block-profile]');
if (blockButton) {
const handle = blockButton.dataset.blockProfile;
if (!isServerBacked()) {
const item = fallbackData.users.find(user => user.handle === handle) || fallbackData.social.following.find(user => user.handle === handle);
if (item?.can_be_blocked !== false && !fallbackData.social.blocked.some(user => user.handle === handle)) {
fallbackData.social.blocked.push(item);
fallbackData.social.following = fallbackData.social.following.filter(user => user.handle !== handle);
fallbackData.social.followers = fallbackData.social.followers.filter(user => user.handle !== handle);
}
}
post('blockProfile', { handle });
render();
return;
}
const unblockButton = event.target.closest('[data-unblock-profile]');
if (unblockButton) {
const handle = unblockButton.dataset.unblockProfile;
if (!isServerBacked()) {
fallbackData.social.blocked = fallbackData.social.blocked.filter(user => user.handle !== handle);
}
post('unblockProfile', { handle });
render();
return;
}
const createPostButton = event.target.closest('[data-create-post]');
if (createPostButton) {
const feedType = createPostButton.dataset.createPost;
const form = createPostButton.closest('.compose');
const textarea = form?.querySelector('textarea');
const body = textarea?.value.trim();
const mediaInput = form?.querySelector('[data-compose-media]');
const mediaUrl = mediaInput?.value.trim() || state.composeMedia[feedType] || '';
if (!body && !mediaUrl) return;
if (!isServerBacked()) {
const profile = activeProfile();
fallbackData.posts.unshift({
id: Date.now(),
feed_type: feedType,
author: {
id: profile.id,
handle: profile.handle,
display_name: profile.display_name,
avatar_url: profile.avatar_url,
is_verified: profile.is_verified
},
body,
media_url: mediaUrl,
created_at: 'Gerade eben',
likes: 0,
comments: 0,
comments_list: [],
can_delete: true
});
}
textarea.value = '';
if (mediaInput) mediaInput.value = '';
state.composeMedia[feedType] = '';
post('createPost', { feedType, body, mediaUrl });
render();
return;
}
const profileButton = event.target.closest('[data-profile]');
if (profileButton) {
const profileId = Number(profileButton.dataset.profile);
if (state.data) state.data.activeProfileId = profileId;
fallbackData.activeProfileId = profileId;
state.viewedProfileHandle = null;
state.editingProfile = false;
post('setActiveProfile', { profileId });
render();
return;
}
if (event.target.closest('[data-edit-profile]')) {
state.editingProfile = true;
render();
return;
}
// Upload-Buttons sind jetzt – kein programmatischer Trigger nötig
if (event.target.closest('[data-cancel-profile-edit]')) {
state.editingProfile = false;
render();
return;
}
if (event.target.closest('[data-save-profile]')) {
const payload = {
displayName: document.getElementById('edit-display-name')?.value.trim() || '',
avatarUrl: document.getElementById('edit-avatar-url')?.value.trim() || '',
bannerUrl: document.getElementById('edit-banner-url')?.value.trim() || '',
bio: document.getElementById('edit-bio')?.value.trim() || ''
};
if (!payload.displayName) return;
if (!isServerBacked()) {
const profile = activeProfile();
profile.display_name = payload.displayName;
profile.avatar_url = payload.avatarUrl;
profile.banner_url = payload.bannerUrl;
profile.bio = payload.bio;
}
state.editingProfile = false;
post('updateProfile', payload);
render();
return;
}
if (event.target.closest('[data-create-marketplace]')) {
let title = document.getElementById('market-title')?.value.trim() || '';
let description = document.getElementById('market-description')?.value.trim() || '';
const priceLabel = document.getElementById('market-price')?.value.trim() || '';
const mediaUrl = state.composeMedia.market || '';
if (!title && !description) return;
if (!title) title = description.slice(0, 80);
if (!description) description = title;
if (!isServerBacked()) {
const profile = activeProfile();
fallbackData.marketplace.unshift({
id: Date.now(),
title,
body: description,
price: Number(priceLabel.match(/\d+/)?.[0] || 0),
price_label: priceLabel,
created: 0,
media_url: mediaUrl,
can_delete: true,
author: {
handle: profile.handle,
display_name: profile.display_name,
avatar_url: profile.avatar_url,
email: profile.email_contact || data().account?.mailAddress || '',
is_verified: profile.is_verified
}
});
}
state.composeMedia.market = '';
post('createMarketplaceEntry', { title, description, priceLabel, mediaUrl });
render();
return;
}
const likeButton = event.target.closest('[data-like-post]');
if (likeButton) {
const postId = Number(likeButton.dataset.likePost);
if (state.likedPosts.has(postId)) {
state.likedPosts.delete(postId);
state.likeDeltas[postId] = Number(state.likeDeltas[postId] || 0) - 1;
} else {
state.likedPosts.add(postId);
state.likeDeltas[postId] = Number(state.likeDeltas[postId] || 0) + 1;
}
post('togglePostLike', { postId });
render();
return;
}
const commentsButton = event.target.closest('[data-comments-post]');
if (commentsButton) {
const postId = Number(commentsButton.dataset.commentsPost);
if (state.commentsOpen.has(postId)) state.commentsOpen.delete(postId);
else state.commentsOpen.add(postId);
render();
return;
}
const deletePostButton = event.target.closest('[data-delete-post]');
if (deletePostButton) {
const postId = Number(deletePostButton.dataset.deletePost);
if (!isServerBacked()) {
fallbackData.posts = fallbackData.posts.filter(item => Number(item.id) !== postId);
}
post('deleteOwnPost', { postId });
render();
return;
}
const sendCommentButton = event.target.closest('[data-send-comment]');
if (sendCommentButton) {
const postId = Number(sendCommentButton.dataset.sendComment);
const input = document.querySelector(`[data-comment-input="${postId}"]`);
const body = input?.value.trim();
if (body) {
const profile = activeProfile();
commentsForPost(postId).push({
id: `${postId}-${Date.now()}`,
author: profile.handle || 'ich',
body,
likes: 0,
can_delete: true
});
post('createComment', { postId, body });
render();
}
return;
}
const likeCommentButton = event.target.closest('[data-like-comment]');
if (likeCommentButton) {
const commentId = likeCommentButton.dataset.likeComment;
if (state.likedComments.has(commentId)) {
state.likedComments.delete(commentId);
state.commentLikeDeltas[commentId] = Number(state.commentLikeDeltas[commentId] || 0) - 1;
} else {
state.likedComments.add(commentId);
state.commentLikeDeltas[commentId] = Number(state.commentLikeDeltas[commentId] || 0) + 1;
}
post('toggleCommentLike', { commentId });
render();
return;
}
const deleteCommentButton = event.target.closest('[data-delete-comment]');
if (deleteCommentButton) {
const commentId = deleteCommentButton.dataset.deleteComment;
if (!isServerBacked()) {
for (const postId of Object.keys(state.postComments)) {
state.postComments[postId] = state.postComments[postId].filter(comment => String(comment.id) !== String(commentId));
}
}
post('deleteOwnComment', { commentId });
render();
return;
}
const deleteMarketplaceButton = event.target.closest('[data-delete-marketplace]');
if (deleteMarketplaceButton) {
const entryId = Number(deleteMarketplaceButton.dataset.deleteMarketplace);
if (!isServerBacked()) {
fallbackData.marketplace = fallbackData.marketplace.filter(item => Number(item.id) !== entryId);
}
post('deleteMarketplaceEntry', { entryId });
render();
return;
}
const calendarButton = event.target.closest('[data-calendar-day]');
if (calendarButton) {
state.selectedCalendarDay = Number(calendarButton.dataset.calendarDay);
render();
return;
}
const createEventButton = event.target.closest('[data-create-event]');
if (createEventButton) {
const date = createEventButton.dataset.createEvent;
const time = document.getElementById('event-time')?.value || '20:00';
const title = document.getElementById('event-title')?.value.trim();
const location = document.getElementById('event-location')?.value.trim();
if (!title) return;
if (!isServerBacked()) {
const day = (fallbackData.calendarDays || []).find(item => item.date === date);
const profile = activeProfile();
if (day) {
day.events.push({
id: Date.now(),
time,
title,
location,
author: profile.handle,
can_delete: true
});
}
}
post('createEvent', { date, time, title, location });
render();
return;
}
const deleteEventButton = event.target.closest('[data-delete-event]');
if (deleteEventButton) {
const eventId = Number(deleteEventButton.dataset.deleteEvent);
if (!isServerBacked()) {
for (const day of fallbackData.calendarDays || []) {
day.events = day.events.filter(event => Number(event.id) !== eventId);
}
}
post('deleteEvent', { eventId });
render();
return;
}
const foundProfile = event.target.closest('[data-found-profile]');
if (foundProfile) {
openProfile(foundProfile.dataset.foundProfile);
}
});
document.addEventListener('input', event => {
if (event.target === globalSearch) {
state.searchQuery = globalSearch.value;
renderSearchResults();
return;
}
if (event.target?.id === 'market-search') {
state.marketQuery = event.target.value;
refreshMarketplaceResults();
return;
}
if (event.target?.dataset?.composeMedia) {
state.composeMedia[event.target.dataset.composeMedia] = event.target.value.trim();
}
});
function setComposeUploadTarget(labelEl) {
const form = labelEl.closest('.compose') || labelEl.closest('.upload-row')?.closest('form');
const mediaInput = form?.querySelector('[data-compose-media]');
state.pendingUploadTarget = mediaInput ? `compose:${mediaInput.dataset.composeMedia}` : null;
}
mediaFileInput.addEventListener('change', () => {
const file = mediaFileInput.files?.[0];
const target = state.pendingUploadTarget;
mediaFileInput.value = '';
if (!file || !target) return;
if (file.size > 2 * 1024 * 1024) {
showToast('Datei ist groesser als 2 MB.', 'error');
return;
}
const reader = new FileReader();
reader.addEventListener('load', () => {
const result = String(reader.result || '');
const base64 = result.includes(',') ? result.split(',').pop() : result;
post('uploadMedia', {
target,
name: file.name,
mime: file.type,
size: file.size,
data: base64
});
});
reader.readAsDataURL(file);
});
document.addEventListener('change', event => {
if (event.target?.id === 'market-sort') {
state.marketSort = event.target.value;
refreshMarketplaceResults();
}
});
window.addEventListener('message', event => {
const msg = event.data || {};
if (msg.action === 'open') show(true, msg.mode || 'phone');
if (msg.action === 'close') show(false);
if (msg.action === 'data' && msg.ok) {
const d = msg.data || {};
// Sub-Actions: Registration-Overlay NICHT schließen, Hauptzustand NICHT überschreiben
if (d.action === 'handleCheck') {
handleCheckResult(d);
return;
}
if (d.action === 'registerError') {
const reasonMap = { handle_taken: 'Dieser Handle ist bereits vergeben.', reserved: 'Dieser Handle ist reserviert.' };
showRegError(reasonMap[d.reason] || 'Registrierung fehlgeschlagen.');
return;
}
// Registrierungsflow: Privatprofil fehlt noch
if (d.needs_registration) {
showRegistration(d);
return;
}
// Normaler Bootstrap
state.data = d;
hydrateInteractionState();
state.editingProfile = false;
hideRegistration();
render();
}
if (msg.action === 'notify' && msg.message) {
showToast(msg.message, msg.type || 'info');
}
if (msg.action === 'mediaUploaded' && msg.ok && msg.url) {
applyUploadedMedia(msg.target, msg.url);
showToast('Bild hochgeladen.', 'success');
}
if (msg.action === 'mediaUploaded' && !msg.ok) {
showToast('Upload fehlgeschlagen.', 'error');
}
});
// ── Registrierungs-UI ─────────────────────────────────────────────────────────
let _regData = null;
let _handleCheckTimer = null;
function showRegistration(data) {
_regData = data;
let el = document.getElementById('bleeter-registration');
if (!el) {
el = document.createElement('div');
el.id = 'bleeter-registration';
el.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.92);z-index:9999;display:flex;align-items:center;justify-content:center;';
document.body.appendChild(el);
}
const noMail = data.reason === 'no_mail';
el.innerHTML = `
🐦
Lifeinvader Bleeter
Erstelle deinen Account
${noMail ? `
📬
Keine IC-Mailadresse hinterlegt
Bleeter benötigt eine persönliche IC-Mailadresse als Login.
Bitte richte deine Mailadresse zuerst im IC-PC → Mail-App ein.
Deine Mailadresse ist dauerhaft mit deinem Bleeter-Profil verknüpft.
` : `
IC-Mailadresse
${data.mail || '–'}
Anzeigename
Account erstellen
`}
`;
el.style.display = 'flex';
// Handle vorab prüfen falls vorausgefüllt
if (!noMail && data.suggested_handle) {
setTimeout(() => onHandleInput(data.suggested_handle), 500);
}
}
function hideRegistration() {
const el = document.getElementById('bleeter-registration');
if (el) el.style.display = 'none';
}
function showRegError(msg) {
const el = document.getElementById('reg-error');
if (el) { el.textContent = msg; el.style.display = 'block'; }
const btn = document.querySelector('#bleeter-registration button[onclick="submitRegistration()"]');
if (btn) btn.disabled = false;
}
function onHandleInput(val) {
clearTimeout(_handleCheckTimer);
const status = document.getElementById('reg-handle-status');
val = val.toLowerCase().trim();
if (!status) return;
if (val.length < 2) { status.style.color = '#888'; status.textContent = 'Mindestens 2 Zeichen'; return; }
if (val.length > 32) { status.style.color = '#e05050'; status.textContent = 'Maximal 32 Zeichen'; return; }
if (!/^[a-z0-9._\-]+$/.test(val)) { status.style.color = '#e05050'; status.textContent = 'Ungültige Zeichen (nur a-z, 0-9, . _ -)'; return; }
status.style.color = '#888'; status.textContent = 'Prüfe Verfügbarkeit…';
_handleCheckTimer = setTimeout(() => {
post('checkHandle', { handle: val });
}, 500);
}
function handleCheckResult(data) {
const status = document.getElementById('reg-handle-status');
if (!status) return;
if (data.available) {
status.style.color = '#5dca5d'; status.textContent = '✓ Handle verfügbar';
} else {
const msg = { too_short:'Zu kurz', too_long:'Zu lang', invalid_chars:'Ungültige Zeichen', reserved:'Reserviert', handle_taken:'Bereits vergeben' };
status.style.color = '#e05050'; status.textContent = '✗ ' + (msg[data.reason] || 'Nicht verfügbar');
}
}
function submitRegistration() {
const handle = (document.getElementById('reg-handle')?.value || '').toLowerCase().trim();
const display = (document.getElementById('reg-display')?.value || '').trim();
const status = document.getElementById('reg-handle-status');
if (!handle || (status && status.style.color !== 'rgb(93, 202, 93)')) {
showRegError('Bitte wähle einen verfügbaren Handle.'); return;
}
const btn = document.querySelector('#bleeter-registration button[onclick="submitRegistration()"]');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Wird erstellt…'; }
const errEl = document.getElementById('reg-error');
if (errEl) errEl.style.display = 'none';
post('registerAccount', { handle, displayName: display || handle });
}
function applyUploadedMedia(target, url) {
if (target === 'profile-avatar') {
const input = document.getElementById('edit-avatar-url');
if (input) input.value = url;
return;
}
if (target === 'profile-banner') {
const input = document.getElementById('edit-banner-url');
if (input) input.value = url;
return;
}
if (String(target || '').startsWith('compose:')) {
const feedType = target.split(':')[1];
state.composeMedia[feedType] = url;
const input = document.querySelector(`[data-compose-media="${feedType}"]`);
if (input) input.value = url;
}
}
function showToast(message, type = 'info') {
const layer = document.getElementById('toast-layer');
if (!layer) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
layer.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3600);
}
function closeBleeter() {
post('close');
show(false);
}
document.addEventListener('keydown', event => {
if (event.key === 'Escape') closeBleeter();
});
const topCloseButton = document.getElementById('top-close');
if (topCloseButton) topCloseButton.addEventListener('click', closeBleeter);
if (!window.invokeNative) {
show(true);
}