Spaces:
Running
Running
Update static/script.js
Browse files- static/script.js +742 -39
static/script.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
(function () {
|
| 7 |
'use strict';
|
| 8 |
|
| 9 |
-
const APP_VERSION = '
|
| 10 |
|
| 11 |
// ============================================================
|
| 12 |
// Safe storage
|
|
@@ -129,6 +129,7 @@
|
|
| 129 |
refreshBlocksBtn: $('refresh-blocks-btn'), blockedUsersList: $('blocked-users-list'),
|
| 130 |
|
| 131 |
settingsDisplayName: $('settings-display-name'), settingsUsername: $('settings-username'),
|
|
|
|
| 132 |
settingsAvatarPreview: $('settings-avatar-preview'), settingsAvatarImg: $('settings-avatar-img'),
|
| 133 |
avatarInput: $('avatar-input'), avatarUploadBtn: $('avatar-upload-btn'),
|
| 134 |
themeOptions: document.querySelectorAll('.theme-option'),
|
|
@@ -499,6 +500,7 @@
|
|
| 499 |
store.set('token', State.token);
|
| 500 |
showChat();
|
| 501 |
connectWebSocket();
|
|
|
|
| 502 |
}
|
| 503 |
|
| 504 |
function showAuthError(msg) {
|
|
@@ -2369,16 +2371,14 @@
|
|
| 2369 |
await uploadChunked(file);
|
| 2370 |
});
|
| 2371 |
|
| 2372 |
-
async function
|
| 2373 |
if (file.size > 500 * 1024 * 1024) {
|
| 2374 |
-
|
| 2375 |
-
return;
|
| 2376 |
}
|
| 2377 |
const CHUNK_SIZE = 5 * 1024 * 1024;
|
| 2378 |
const totalChunks = Math.max(1, Math.ceil(file.size / CHUNK_SIZE));
|
| 2379 |
const uploadId = generateId();
|
| 2380 |
-
|
| 2381 |
-
updateProgress(0, file.name, file.size);
|
| 2382 |
|
| 2383 |
for (let i = 0; i < totalChunks; i++) {
|
| 2384 |
const blob = file.slice(i * CHUNK_SIZE, Math.min((i + 1) * CHUNK_SIZE, file.size));
|
|
@@ -2387,36 +2387,41 @@
|
|
| 2387 |
const url = `${API}/api/upload/chunk?chunk_index=${i}&total_chunks=${totalChunks}` +
|
| 2388 |
`&file_name=${encodeURIComponent(file.name)}&file_type=${encodeURIComponent(file.type || 'application/octet-stream')}` +
|
| 2389 |
`&file_size=${file.size}&upload_id=${uploadId}`;
|
| 2390 |
-
|
| 2391 |
-
|
| 2392 |
-
|
| 2393 |
-
|
| 2394 |
-
|
| 2395 |
-
|
| 2396 |
-
|
| 2397 |
-
|
| 2398 |
-
|
| 2399 |
-
|
| 2400 |
-
|
| 2401 |
-
|
| 2402 |
-
|
| 2403 |
-
|
| 2404 |
-
|
| 2405 |
-
|
| 2406 |
-
|
| 2407 |
-
showUploadPreview(State.pendingFile);
|
| 2408 |
-
DOM.uploadProgress.classList.add('hidden');
|
| 2409 |
-
DOM.sendBtn.disabled = false;
|
| 2410 |
-
DOM.messageInput.focus();
|
| 2411 |
-
return;
|
| 2412 |
-
}
|
| 2413 |
-
} catch (err) {
|
| 2414 |
-
showToast('Upload failed: ' + err.message, 'error');
|
| 2415 |
-
DOM.uploadProgress.classList.add('hidden');
|
| 2416 |
-
return;
|
| 2417 |
}
|
| 2418 |
}
|
| 2419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2420 |
}
|
| 2421 |
|
| 2422 |
function updateProgress(percent, filename, size) {
|
|
@@ -2894,6 +2899,9 @@
|
|
| 2894 |
function openSettings() {
|
| 2895 |
DOM.settingsDisplayName.value = State.user?.display_name || '';
|
| 2896 |
DOM.settingsUsername.value = State.user?.username || '';
|
|
|
|
|
|
|
|
|
|
| 2897 |
DOM.pwCurrent.value = '';
|
| 2898 |
DOM.pwNew.value = '';
|
| 2899 |
DOM.pwMsg.textContent = '';
|
|
@@ -2928,22 +2936,37 @@
|
|
| 2928 |
|
| 2929 |
async function saveSettings() {
|
| 2930 |
const newName = DOM.settingsDisplayName.value.trim();
|
| 2931 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2932 |
try {
|
| 2933 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2934 |
method: 'PATCH', headers: { 'X-Auth-Token': State.token }
|
| 2935 |
});
|
| 2936 |
if (res.ok) {
|
| 2937 |
-
|
|
|
|
|
|
|
|
|
|
| 2938 |
updateSidebarMe();
|
| 2939 |
renderSidebar();
|
| 2940 |
renderConversationHeader(State.activeConvId);
|
| 2941 |
-
|
|
|
|
| 2942 |
} else {
|
| 2943 |
const data = await res.json().catch(() => ({}));
|
| 2944 |
throw new Error(data.detail || 'Failed');
|
| 2945 |
}
|
| 2946 |
-
} catch (e) { showToast('Failed to update
|
| 2947 |
}
|
| 2948 |
|
| 2949 |
const avatarFile = DOM.avatarInput.files[0];
|
|
@@ -3067,6 +3090,683 @@
|
|
| 3067 |
// ============================================================
|
| 3068 |
// Auth lifecycle
|
| 3069 |
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3070 |
async function init() {
|
| 3071 |
applyPrefs();
|
| 3072 |
DOM.aboutVersion.textContent = APP_VERSION;
|
|
@@ -3078,6 +3778,7 @@
|
|
| 3078 |
State.user = data.user;
|
| 3079 |
showChat();
|
| 3080 |
connectWebSocket();
|
|
|
|
| 3081 |
return;
|
| 3082 |
}
|
| 3083 |
} catch (e) { /* fall through to auth screen */ }
|
|
@@ -3095,6 +3796,8 @@
|
|
| 3095 |
|
| 3096 |
function showChat() {
|
| 3097 |
DOM.authScreen.classList.remove('active');
|
|
|
|
|
|
|
| 3098 |
DOM.chatScreen.classList.add('active');
|
| 3099 |
updateSidebarMe();
|
| 3100 |
}
|
|
|
|
| 6 |
(function () {
|
| 7 |
'use strict';
|
| 8 |
|
| 9 |
+
const APP_VERSION = '3.0.0';
|
| 10 |
|
| 11 |
// ============================================================
|
| 12 |
// Safe storage
|
|
|
|
| 129 |
refreshBlocksBtn: $('refresh-blocks-btn'), blockedUsersList: $('blocked-users-list'),
|
| 130 |
|
| 131 |
settingsDisplayName: $('settings-display-name'), settingsUsername: $('settings-username'),
|
| 132 |
+
settingsBio: $('settings-bio'), settingsLocation: $('settings-location'), settingsWebsite: $('settings-website'),
|
| 133 |
settingsAvatarPreview: $('settings-avatar-preview'), settingsAvatarImg: $('settings-avatar-img'),
|
| 134 |
avatarInput: $('avatar-input'), avatarUploadBtn: $('avatar-upload-btn'),
|
| 135 |
themeOptions: document.querySelectorAll('.theme-option'),
|
|
|
|
| 500 |
store.set('token', State.token);
|
| 501 |
showChat();
|
| 502 |
connectWebSocket();
|
| 503 |
+
initSocial();
|
| 504 |
}
|
| 505 |
|
| 506 |
function showAuthError(msg) {
|
|
|
|
| 2371 |
await uploadChunked(file);
|
| 2372 |
});
|
| 2373 |
|
| 2374 |
+
async function uploadFileChunked(file, onProgress) {
|
| 2375 |
if (file.size > 500 * 1024 * 1024) {
|
| 2376 |
+
throw new Error('File too large (max 500MB)');
|
|
|
|
| 2377 |
}
|
| 2378 |
const CHUNK_SIZE = 5 * 1024 * 1024;
|
| 2379 |
const totalChunks = Math.max(1, Math.ceil(file.size / CHUNK_SIZE));
|
| 2380 |
const uploadId = generateId();
|
| 2381 |
+
if (onProgress) onProgress(0, file.name, file.size, totalChunks);
|
|
|
|
| 2382 |
|
| 2383 |
for (let i = 0; i < totalChunks; i++) {
|
| 2384 |
const blob = file.slice(i * CHUNK_SIZE, Math.min((i + 1) * CHUNK_SIZE, file.size));
|
|
|
|
| 2387 |
const url = `${API}/api/upload/chunk?chunk_index=${i}&total_chunks=${totalChunks}` +
|
| 2388 |
`&file_name=${encodeURIComponent(file.name)}&file_type=${encodeURIComponent(file.type || 'application/octet-stream')}` +
|
| 2389 |
`&file_size=${file.size}&upload_id=${uploadId}`;
|
| 2390 |
+
const res = await fetch(url, {
|
| 2391 |
+
method: 'POST', body: formData, headers: { 'X-Auth-Token': State.token }
|
| 2392 |
+
});
|
| 2393 |
+
if (!res.ok) {
|
| 2394 |
+
const data = await res.json().catch(() => ({}));
|
| 2395 |
+
throw new Error(data.detail || 'Upload failed');
|
| 2396 |
+
}
|
| 2397 |
+
const data = await res.json();
|
| 2398 |
+
const percent = ((i + 1) / totalChunks) * 100;
|
| 2399 |
+
if (onProgress) onProgress(percent, file.name, file.size, totalChunks);
|
| 2400 |
+
if (data.status === 'complete') {
|
| 2401 |
+
return {
|
| 2402 |
+
file_path: data.file_path,
|
| 2403 |
+
file_type: data.file_type || file.type,
|
| 2404 |
+
file_name: data.file_name,
|
| 2405 |
+
file_size: data.file_size
|
| 2406 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2407 |
}
|
| 2408 |
}
|
| 2409 |
+
throw new Error('Upload did not complete');
|
| 2410 |
+
}
|
| 2411 |
+
|
| 2412 |
+
async function uploadChunked(file) {
|
| 2413 |
+
try {
|
| 2414 |
+
DOM.uploadProgress.classList.remove('hidden');
|
| 2415 |
+
updateProgress(0, file.name, file.size);
|
| 2416 |
+
State.pendingFile = await uploadFileChunked(file, (pct) => updateProgress(pct, file.name, file.size));
|
| 2417 |
+
showUploadPreview(State.pendingFile);
|
| 2418 |
+
DOM.uploadProgress.classList.add('hidden');
|
| 2419 |
+
DOM.sendBtn.disabled = false;
|
| 2420 |
+
DOM.messageInput.focus();
|
| 2421 |
+
} catch (err) {
|
| 2422 |
+
showToast('Upload failed: ' + err.message, 'error');
|
| 2423 |
+
DOM.uploadProgress.classList.add('hidden');
|
| 2424 |
+
}
|
| 2425 |
}
|
| 2426 |
|
| 2427 |
function updateProgress(percent, filename, size) {
|
|
|
|
| 2899 |
function openSettings() {
|
| 2900 |
DOM.settingsDisplayName.value = State.user?.display_name || '';
|
| 2901 |
DOM.settingsUsername.value = State.user?.username || '';
|
| 2902 |
+
DOM.settingsBio.value = State.user?.bio || '';
|
| 2903 |
+
DOM.settingsLocation.value = State.user?.location || '';
|
| 2904 |
+
DOM.settingsWebsite.value = State.user?.website || '';
|
| 2905 |
DOM.pwCurrent.value = '';
|
| 2906 |
DOM.pwNew.value = '';
|
| 2907 |
DOM.pwMsg.textContent = '';
|
|
|
|
| 2936 |
|
| 2937 |
async function saveSettings() {
|
| 2938 |
const newName = DOM.settingsDisplayName.value.trim();
|
| 2939 |
+
const newBio = DOM.settingsBio.value.trim();
|
| 2940 |
+
const newLocation = DOM.settingsLocation.value.trim();
|
| 2941 |
+
const newWebsite = DOM.settingsWebsite.value.trim();
|
| 2942 |
+
const profileChanged = (newName && newName !== State.user?.display_name) ||
|
| 2943 |
+
newBio !== (State.user?.bio || '') || newLocation !== (State.user?.location || '') ||
|
| 2944 |
+
newWebsite !== (State.user?.website || '');
|
| 2945 |
+
if (profileChanged) {
|
| 2946 |
try {
|
| 2947 |
+
const params = [];
|
| 2948 |
+
if (newName && newName !== State.user?.display_name) params.push('display_name=' + encodeURIComponent(newName));
|
| 2949 |
+
if (newBio !== (State.user?.bio || '')) params.push('bio=' + encodeURIComponent(newBio));
|
| 2950 |
+
if (newLocation !== (State.user?.location || '')) params.push('location=' + encodeURIComponent(newLocation));
|
| 2951 |
+
if (newWebsite !== (State.user?.website || '')) params.push('website=' + encodeURIComponent(newWebsite));
|
| 2952 |
+
const res = await fetch(`${API}/api/profile?${params.join('&')}`, {
|
| 2953 |
method: 'PATCH', headers: { 'X-Auth-Token': State.token }
|
| 2954 |
});
|
| 2955 |
if (res.ok) {
|
| 2956 |
+
const data = await res.json();
|
| 2957 |
+
if (data.user) { State.user = data.user; }
|
| 2958 |
+
if (newName) State.user.display_name = newName;
|
| 2959 |
+
State.user.bio = newBio; State.user.location = newLocation; State.user.website = newWebsite;
|
| 2960 |
updateSidebarMe();
|
| 2961 |
renderSidebar();
|
| 2962 |
renderConversationHeader(State.activeConvId);
|
| 2963 |
+
updateSocialMe();
|
| 2964 |
+
showToast('Profile updated', 'success');
|
| 2965 |
} else {
|
| 2966 |
const data = await res.json().catch(() => ({}));
|
| 2967 |
throw new Error(data.detail || 'Failed');
|
| 2968 |
}
|
| 2969 |
+
} catch (e) { showToast('Failed to update profile', 'error'); }
|
| 2970 |
}
|
| 2971 |
|
| 2972 |
const avatarFile = DOM.avatarInput.files[0];
|
|
|
|
| 3090 |
// ============================================================
|
| 3091 |
// Auth lifecycle
|
| 3092 |
// ============================================================
|
| 3093 |
+
// ============================================================
|
| 3094 |
+
// Social feed page (Twitter/X-like)
|
| 3095 |
+
// ============================================================
|
| 3096 |
+
const MAX_POST_MEDIA = 4;
|
| 3097 |
+
const DEFAULT_SOCIAL_SETTINGS = {
|
| 3098 |
+
autoplay: false, muted: true, hideReposts: false, compact: false,
|
| 3099 |
+
live: true, badges: true, defaultFeed: 'home'
|
| 3100 |
+
};
|
| 3101 |
+
|
| 3102 |
+
function loadSocialSettings() {
|
| 3103 |
+
let out = { ...DEFAULT_SOCIAL_SETTINGS };
|
| 3104 |
+
try {
|
| 3105 |
+
const raw = store.get('social-settings');
|
| 3106 |
+
if (raw) Object.assign(out, JSON.parse(raw));
|
| 3107 |
+
} catch (e) { /* defaults */ }
|
| 3108 |
+
return out;
|
| 3109 |
+
}
|
| 3110 |
+
|
| 3111 |
+
function saveSocialSettings() {
|
| 3112 |
+
store.set('social-settings', JSON.stringify(SocialState.settings));
|
| 3113 |
+
}
|
| 3114 |
+
|
| 3115 |
+
const SocialState = {
|
| 3116 |
+
inited: false, view: 'home', feedCursor: null,
|
| 3117 |
+
media: [], modalMedia: [], modalMode: 'compose',
|
| 3118 |
+
replyTarget: null, quoteTarget: null, profileUser: null,
|
| 3119 |
+
posts: new Map(), following: new Set(), isSearching: false,
|
| 3120 |
+
searchTimer: null, pollTimer: null, editingPost: null,
|
| 3121 |
+
settings: loadSocialSettings()
|
| 3122 |
+
};
|
| 3123 |
+
|
| 3124 |
+
function _el(id) { return document.getElementById(id); }
|
| 3125 |
+
|
| 3126 |
+
function socialInitial(name) { return initialOf(name || '?'); }
|
| 3127 |
+
|
| 3128 |
+
function socialAvatarHtml(user, cls) {
|
| 3129 |
+
const url = user && user.avatar_path ? fileUrl(user.avatar_path) : '';
|
| 3130 |
+
if (url) return `<img class="${cls || ''}" src="${escapeHtml(url)}" alt="" loading="lazy">`;
|
| 3131 |
+
const name = displayNameOf(user);
|
| 3132 |
+
return `<span class="social-avatar-initial ${avatarColorClass('social-' + (name||'?').toLowerCase())}">${escapeHtml(initialOf(name))}</span>`;
|
| 3133 |
+
}
|
| 3134 |
+
|
| 3135 |
+
function socialTime(ms) {
|
| 3136 |
+
if (!ms) return '';
|
| 3137 |
+
const d = new Date(ms), n = new Date();
|
| 3138 |
+
const diff = n - d;
|
| 3139 |
+
if (diff < 60000) return 'now';
|
| 3140 |
+
if (diff < 3600000) return Math.floor(diff/60000) + 'm';
|
| 3141 |
+
if (d.toDateString() === n.toDateString()) return d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
| 3142 |
+
if (diff < 86400000 * 7) return d.toLocaleDateString([], {weekday:'short'}) + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
| 3143 |
+
return d.toLocaleDateString([], {month:'short', day:'numeric'});
|
| 3144 |
+
}
|
| 3145 |
+
|
| 3146 |
+
function socialCount(n) {
|
| 3147 |
+
if (!n) return '0';
|
| 3148 |
+
if (n >= 1000000) return (n/1000000).toFixed(1).replace(/\.0$/,'') + 'M';
|
| 3149 |
+
if (n >= 1000) return (n/1000).toFixed(1).replace(/\.0$/,'') + 'K';
|
| 3150 |
+
return String(n);
|
| 3151 |
+
}
|
| 3152 |
+
|
| 3153 |
+
function linkifySocial(text) {
|
| 3154 |
+
if (!text) return '';
|
| 3155 |
+
let html = escapeHtml(text);
|
| 3156 |
+
html = html.replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 3157 |
+
html = html.replace(/(^|[\s(])(#([A-Za-z0-9_]+))/g,
|
| 3158 |
+
(m, pre, tag, body) => pre + `<a class="social-hashtag" data-hashtag="${encodeURIComponent(body)}" href="javascript:void(0)">#${escapeHtml(body)}</a>`);
|
| 3159 |
+
html = html.replace(/(^|[\s(])(@([A-Za-z0-9_]{1,30}))/g,
|
| 3160 |
+
(m, pre, handle, body) => pre + `<a class="social-mention" data-username="${encodeURIComponent(body)}" href="javascript:void(0)">@${escapeHtml(body)}</a>`);
|
| 3161 |
+
html = html.replace(/(https?:\/\/[^\s<]+)/g,
|
| 3162 |
+
(u) => `<a href="${escapeHtml(u)}" target="_blank" rel="noopener noreferrer">${escapeHtml(u)}</a>`);
|
| 3163 |
+
return html;
|
| 3164 |
+
}
|
| 3165 |
+
|
| 3166 |
+
function socialMediaHtml(media) {
|
| 3167 |
+
if (!media || !media.length) return '';
|
| 3168 |
+
const items = media.map(m => {
|
| 3169 |
+
const fp = m.file_path || '';
|
| 3170 |
+
const name = (m.file_name || fp || '').split('/').pop();
|
| 3171 |
+
const type = (m.file_type || '').toLowerCase();
|
| 3172 |
+
const img = type.startsWith('image/') || /\.(jpg|jpeg|png|gif|webp|bmp|avif)$/i.test(fp);
|
| 3173 |
+
const video = type.startsWith('video/') || /\.(mp4|webm|mov|ogg|m4v|mpeg|mpg)$/i.test(fp);
|
| 3174 |
+
let c = '';
|
| 3175 |
+
const videoAttrs = SocialState.settings.autoplay ? (SocialState.settings.muted ? ' autoplay muted loop ' : ' autoplay ') : '';
|
| 3176 |
+
if (img) c = `<img src="${escapeHtml(fileUrl(fp))}" alt="${escapeHtml(name)}" loading="lazy" data-media-image="${escapeHtml(fileUrl(fp))}" data-media-name="${escapeHtml(name)}">`;
|
| 3177 |
+
else if (video) c = `<video controls preload="metadata" src="${escapeHtml(fileUrl(fp))}" ${videoAttrs}></video>`;
|
| 3178 |
+
else c = `<a class="social-file" href="${escapeHtml(fileUrl(fp))}" target="_blank" rel="noopener"><i class="fas fa-file"></i> ${escapeHtml(name)}</a>`;
|
| 3179 |
+
return `<div class="social-media-item${img ? ' img' : ''}">${c}</div>`;
|
| 3180 |
+
}).join('');
|
| 3181 |
+
return `<div class="social-post-media">${items}</div>`;
|
| 3182 |
+
}
|
| 3183 |
+
|
| 3184 |
+
function socialPostHtml(post) {
|
| 3185 |
+
if (!post) return '';
|
| 3186 |
+
const author = post.author || {};
|
| 3187 |
+
const isOwn = State.user && author.id === State.user.id;
|
| 3188 |
+
const repostLabel = post.reposter
|
| 3189 |
+
? `<div class="social-repost-label"><i class="fas fa-retweet"></i> ${escapeHtml(post.reposter.username || '')} reposted</div>`
|
| 3190 |
+
: (post.reposter_id ? `<div class="social-repost-label"><i class="fas fa-retweet"></i> Reposted</div>` : '');
|
| 3191 |
+
const quote = post.quote ? `
|
| 3192 |
+
<div class="social-quote">
|
| 3193 |
+
<div class="social-post-head"><strong>${escapeHtml(displayNameOf(post.quote.author))}</strong>
|
| 3194 |
+
<span class="social-username">@${escapeHtml(post.quote.author.username || '')}</span></div>
|
| 3195 |
+
<div class="social-post-body">${linkifySocial(post.quote.body || '')}</div>
|
| 3196 |
+
</div>` : '';
|
| 3197 |
+
const actions = `
|
| 3198 |
+
<div class="social-post-actions">
|
| 3199 |
+
<button class="sp-action" data-action="reply" data-id="${post.id}" title="Reply">
|
| 3200 |
+
<i class="fas fa-comment"></i><span>${socialCount(post.reply_count || 0)}</span>
|
| 3201 |
+
</button>
|
| 3202 |
+
<button class="sp-action${post.reposted ? ' active' : ''}" data-action="repost" data-id="${post.id}" title="Repost">
|
| 3203 |
+
<i class="fas fa-retweet"></i><span>${socialCount(post.repost_count || 0)}</span>
|
| 3204 |
+
</button>
|
| 3205 |
+
<button class="sp-action${post.liked ? ' active' : ''}" data-action="like" data-id="${post.id}" title="Like">
|
| 3206 |
+
<i class="fas fa-heart"></i><span>${socialCount(post.like_count || 0)}</span>
|
| 3207 |
+
</button>
|
| 3208 |
+
<button class="sp-action${post.bookmarked ? ' active' : ''}" data-action="bookmark" data-id="${post.id}" title="Bookmark">
|
| 3209 |
+
<i class="fas fa-bookmark"></i><span>${socialCount(post.bookmark_count || 0)}</span>
|
| 3210 |
+
</button>
|
| 3211 |
+
<button class="sp-action" data-action="quote" data-id="${post.id}" title="Quote">
|
| 3212 |
+
<i class="fas fa-quote-right"></i><span></span>
|
| 3213 |
+
</button>
|
| 3214 |
+
${isOwn ? `<button class="sp-action" data-action="edit" data-id="${post.id}" title="Edit"><i class="fas fa-pen"></i></button>` : ''}
|
| 3215 |
+
${isOwn ? `<button class="sp-action danger" data-action="delete" data-id="${post.id}" title="Delete"><i class="fas fa-trash"></i></button>` : ''}
|
| 3216 |
+
${!isOwn ? `<button class="sp-action" data-action="profile" data-username="${escapeHtml(author.username || '')}" title="Profile"><i class="fas fa-user"></i></button>` : ''}
|
| 3217 |
+
<button class="sp-action" data-action="share" data-id="${post.id}" title="Copy link"><i class="fas fa-link"></i></button>
|
| 3218 |
+
</div>`;
|
| 3219 |
+
return `<article class="social-post" data-post-id="${post.id}">
|
| 3220 |
+
<div class="social-avatar${author.avatar_path ? '' : ' ah'}">${socialAvatarHtml(author, 'social-avatar-img')}</div>
|
| 3221 |
+
<div class="social-post-main">
|
| 3222 |
+
${repostLabel}
|
| 3223 |
+
<div class="social-post-head">
|
| 3224 |
+
<a class="social-profile-link" data-username="${escapeHtml(author.username || '')}" href="javascript:void(0)"><strong>${escapeHtml(displayNameOf(author))}</strong>
|
| 3225 |
+
<span class="social-username">@${escapeHtml(author.username || '')}</span></a>
|
| 3226 |
+
<span class="social-dot">·</span><time class="social-time" title="${new Date(post.created_at_ms || 0).toLocaleString()}">${socialTime(post.created_at_ms)}</time>
|
| 3227 |
+
${post.edited_at_ms ? '<span class="social-edited">· edited</span>' : ''}
|
| 3228 |
+
</div>
|
| 3229 |
+
<div class="social-post-body">${linkifySocial(post.body || '')}</div>
|
| 3230 |
+
${socialMediaHtml(post.media)}
|
| 3231 |
+
${quote}
|
| 3232 |
+
${actions}
|
| 3233 |
+
</div>
|
| 3234 |
+
</article>`;
|
| 3235 |
+
}
|
| 3236 |
+
|
| 3237 |
+
function renderSocialPosts(posts, container) {
|
| 3238 |
+
const el = container || _el('social-feed');
|
| 3239 |
+
if (!el) return;
|
| 3240 |
+
if (!posts || !posts.length) {
|
| 3241 |
+
const empty = document.createElement('div');
|
| 3242 |
+
empty.className = 'social-feed-empty';
|
| 3243 |
+
empty.innerHTML = '<i class="fas fa-feather"></i><h3>Nothing here yet</h3><p>Follow people or make the first post.</p>';
|
| 3244 |
+
el.innerHTML = '';
|
| 3245 |
+
el.appendChild(empty);
|
| 3246 |
+
return;
|
| 3247 |
+
}
|
| 3248 |
+
posts.forEach(p => SocialState.posts.set(p.id, p));
|
| 3249 |
+
el.innerHTML = posts.map(socialPostHtml).join('');
|
| 3250 |
+
}
|
| 3251 |
+
|
| 3252 |
+
async function openSocial() {
|
| 3253 |
+
if (!State.user) return;
|
| 3254 |
+
_el('social-screen').classList.add('active');
|
| 3255 |
+
DOM.chatScreen.classList.remove('active');
|
| 3256 |
+
updateSocialMe();
|
| 3257 |
+
if (!SocialState.inited) initSocial();
|
| 3258 |
+
const feed = SocialState.settings.defaultFeed === 'explore' ? 'explore' : SocialState.view || 'home';
|
| 3259 |
+
SocialState.view = feed;
|
| 3260 |
+
await SocialLoad.view(feed);
|
| 3261 |
+
startSocialPoll();
|
| 3262 |
+
const hashMatch = location.hash.match(/^#social\/(\d+)$/);
|
| 3263 |
+
if (hashMatch) { try { const data = await apiGet(`/api/social/posts/${hashMatch[1]}`); renderSocialPosts([data.post]); _el('social-compose').style.display='none'; } catch(e) {} }
|
| 3264 |
+
}
|
| 3265 |
+
|
| 3266 |
+
function backToChat() {
|
| 3267 |
+
SocialState.isSearching = false;
|
| 3268 |
+
SocialState.profileUser = null;
|
| 3269 |
+
stopSocialPoll();
|
| 3270 |
+
_el('social-screen').classList.remove('active');
|
| 3271 |
+
DOM.chatScreen.classList.add('active');
|
| 3272 |
+
updateSidebarMe();
|
| 3273 |
+
}
|
| 3274 |
+
|
| 3275 |
+
function startSocialPoll() {
|
| 3276 |
+
stopSocialPoll();
|
| 3277 |
+
if (!SocialState.settings.live) return;
|
| 3278 |
+
SocialState.pollTimer = setInterval(async () => {
|
| 3279 |
+
if (!_el('social-screen').classList.contains('active') || SocialState.isSearching) return;
|
| 3280 |
+
if (SocialState.view !== 'home' && SocialState.view !== 'explore') return;
|
| 3281 |
+
try { await SocialLoad.feed(SocialState.view, true); } catch (e) { /* non-fatal */ }
|
| 3282 |
+
}, 15000);
|
| 3283 |
+
}
|
| 3284 |
+
|
| 3285 |
+
function stopSocialPoll() {
|
| 3286 |
+
if (SocialState.pollTimer) { clearInterval(SocialState.pollTimer); SocialState.pollTimer = null; }
|
| 3287 |
+
}
|
| 3288 |
+
|
| 3289 |
+
function updateSocialMe() {
|
| 3290 |
+
if (!State.user) return;
|
| 3291 |
+
_el('social-me-name').textContent = displayNameOf(State.user);
|
| 3292 |
+
_el('social-me-username').textContent = '@' + (State.user.username || '');
|
| 3293 |
+
const av = _el('social-me-avatar'), ph = _el('social-me-avatar-ph');
|
| 3294 |
+
if (State.user.avatar_path) { av.src = fileUrl(State.user.avatar_path); av.classList.remove('hidden'); ph.classList.add('hidden'); }
|
| 3295 |
+
else { av.classList.add('hidden'); ph.classList.remove('hidden'); ph.textContent = socialInitial(displayNameOf(State.user)); }
|
| 3296 |
+
const cAv = _el('social-compose-avatar'), cPh = _el('social-compose-avatar-ph');
|
| 3297 |
+
if (State.user.avatar_path) { cAv.src = fileUrl(State.user.avatar_path); cAv.classList.remove('hidden'); cPh.classList.add('hidden'); }
|
| 3298 |
+
else { cAv.classList.add('hidden'); cPh.classList.remove('hidden'); cPh.textContent = socialInitial(displayNameOf(State.user)); }
|
| 3299 |
+
}
|
| 3300 |
+
|
| 3301 |
+
const SOCIAL_MEDIA_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.avif', '.mp4', '.webm', '.mov', '.m4v', '.ogg', '.mpeg', '.mpg'];
|
| 3302 |
+
|
| 3303 |
+
function isSocialMediaFile(file) {
|
| 3304 |
+
if (!file) return false;
|
| 3305 |
+
const t = (file.type || '').toLowerCase();
|
| 3306 |
+
const ext = ('.' + (file.name || '').split('.').pop().toLowerCase());
|
| 3307 |
+
return t.startsWith('image/') || t.startsWith('video/') || SOCIAL_MEDIA_EXTS.includes(ext);
|
| 3308 |
+
}
|
| 3309 |
+
|
| 3310 |
+
async function uploadSocialFile(file, onProgress) {
|
| 3311 |
+
if (!file) return null;
|
| 3312 |
+
if (!isSocialMediaFile(file)) throw new Error('Only images and videos can be attached to posts');
|
| 3313 |
+
if (file.size > 200 * 1024 * 1024) throw new Error('Social media files must be under 200MB');
|
| 3314 |
+
if (!State.user) throw new Error('Not signed in');
|
| 3315 |
+
return await uploadFileChunked(file, onProgress);
|
| 3316 |
+
}
|
| 3317 |
+
|
| 3318 |
+
function socialPreviewMedia() {
|
| 3319 |
+
const list = SocialState.media;
|
| 3320 |
+
const box = _el('social-media-preview');
|
| 3321 |
+
if (!list.length) { box.classList.add('hidden'); box.innerHTML = ''; return; }
|
| 3322 |
+
box.classList.remove('hidden');
|
| 3323 |
+
box.innerHTML = list.map((m, i) => `
|
| 3324 |
+
<div class="social-media-preview-item">
|
| 3325 |
+
${m.file_type && m.file_type.startsWith('video/') ? `<video src="${escapeHtml(m.preview_url || fileUrl(m.file_path))}" muted></video>` : `<img src="${escapeHtml(m.preview_url || fileUrl(m.file_path))}" alt="">`}
|
| 3326 |
+
<button class="btn-icon btn-icon-sm" data-remove-media="${i}"><i class="fas fa-times"></i></button>
|
| 3327 |
+
</div>`).join('');
|
| 3328 |
+
}
|
| 3329 |
+
|
| 3330 |
+
async function addSocialMedia(files, modal) {
|
| 3331 |
+
if (!files || !files.length) return;
|
| 3332 |
+
const progress = modal ? _el('social-compose-modal-progress') : _el('social-upload-progress');
|
| 3333 |
+
const fill = progress ? progress.querySelector('.social-progress-fill') : null;
|
| 3334 |
+
const label = progress ? progress.querySelector('span') : null;
|
| 3335 |
+
if (progress) progress.classList.remove('hidden');
|
| 3336 |
+
try {
|
| 3337 |
+
const target = modal ? SocialState.modalMedia : SocialState.media;
|
| 3338 |
+
for (const f of Array.from(files)) {
|
| 3339 |
+
if (!isSocialMediaFile(f)) { showToast(f.name + ' is not an image or video', 'warning'); continue; }
|
| 3340 |
+
if (target.length >= MAX_POST_MEDIA) { showToast('A post can contain at most ' + MAX_POST_MEDIA + ' media files', 'warning'); break; }
|
| 3341 |
+
const up = await uploadSocialFile(f, (pct, name) => {
|
| 3342 |
+
if (fill) fill.style.width = Math.round(pct) + '%';
|
| 3343 |
+
if (label) label.textContent = 'Uploading ' + name + '… ' + Math.round(pct) + '%';
|
| 3344 |
+
});
|
| 3345 |
+
if (!up) continue;
|
| 3346 |
+
target.push({ file_path: up.file_path, file_name: up.file_name, file_type: up.file_type, file_size: up.file_size, preview_url: URL.createObjectURL(f) });
|
| 3347 |
+
if (fill) fill.style.width = '0%';
|
| 3348 |
+
}
|
| 3349 |
+
socialPreviewMedia();
|
| 3350 |
+
if (modal) socialPreviewModalMedia();
|
| 3351 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3352 |
+
finally {
|
| 3353 |
+
if (progress) progress.classList.add('hidden');
|
| 3354 |
+
if (fill) fill.style.width = '0%';
|
| 3355 |
+
if (label) label.textContent = '';
|
| 3356 |
+
if (modal) _el('social-compose-modal-media').value = ''; else _el('social-media-input').value = '';
|
| 3357 |
+
}
|
| 3358 |
+
}
|
| 3359 |
+
|
| 3360 |
+
function socialPreviewModalMedia() {
|
| 3361 |
+
const list = SocialState.modalMedia;
|
| 3362 |
+
const box = _el('social-compose-modal-media-preview');
|
| 3363 |
+
if (!list.length) { box.classList.add('hidden'); box.innerHTML = ''; return; }
|
| 3364 |
+
box.classList.remove('hidden');
|
| 3365 |
+
box.innerHTML = list.map((m, i) => `
|
| 3366 |
+
<div class="social-media-preview-item">
|
| 3367 |
+
${m.file_type && m.file_type.startsWith('video/') ? `<video src="${escapeHtml(m.preview_url || fileUrl(m.file_path))}" muted></video>` : `<img src="${escapeHtml(m.preview_url || fileUrl(m.file_path))}" alt="">`}
|
| 3368 |
+
<button class="btn-icon btn-icon-sm" data-remove-modal-media="${i}"><i class="fas fa-times"></i></button>
|
| 3369 |
+
</div>`).join('');
|
| 3370 |
+
}
|
| 3371 |
+
|
| 3372 |
+
async function submitSocialPost() {
|
| 3373 |
+
const body = _el('social-post-input').value.trim();
|
| 3374 |
+
if (!body && !SocialState.media.length) return;
|
| 3375 |
+
SocialState.media = SocialState.media.slice(0, MAX_POST_MEDIA);
|
| 3376 |
+
const mediaPayload = SocialState.media.map((m) => { const { preview_url, ...rest } = m; return rest; });
|
| 3377 |
+
const url = `/api/social/posts?body=${encodeURIComponent(body)}&media_json=${encodeURIComponent(JSON.stringify(mediaPayload))}`;
|
| 3378 |
+
try {
|
| 3379 |
+
const data = await apiPost(url);
|
| 3380 |
+
if (data.post) {
|
| 3381 |
+
_el('social-post-input').value = '';
|
| 3382 |
+
SocialState.media = [];
|
| 3383 |
+
socialPreviewMedia();
|
| 3384 |
+
_el('social-post-btn').disabled = true;
|
| 3385 |
+
showToast('Posted!', 'success');
|
| 3386 |
+
await SocialLoad.view(SocialState.view, true);
|
| 3387 |
+
}
|
| 3388 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3389 |
+
}
|
| 3390 |
+
|
| 3391 |
+
async function submitModalSocialPost() {
|
| 3392 |
+
const body = _el('social-compose-modal-input').value.trim();
|
| 3393 |
+
const media = SocialState.modalMedia;
|
| 3394 |
+
if (!body && !media.length) return;
|
| 3395 |
+
try {
|
| 3396 |
+
let data;
|
| 3397 |
+
if (SocialState.editingPost) {
|
| 3398 |
+
if (!body.trim()) throw new Error('Post body cannot be empty');
|
| 3399 |
+
data = await apiPatch(`/api/social/posts/${SocialState.editingPost.id}?body=${encodeURIComponent(body)}`);
|
| 3400 |
+
} else {
|
| 3401 |
+
const q = [];
|
| 3402 |
+
q.push('body=' + encodeURIComponent(body));
|
| 3403 |
+
if (media.length) q.push('media_json=' + encodeURIComponent(JSON.stringify(media.map((m) => { const { preview_url, ...rest } = m; return rest; }))));
|
| 3404 |
+
if (SocialState.replyTarget) q.push('reply_to_id=' + socialInt(SocialState.replyTarget.id));
|
| 3405 |
+
if (SocialState.quoteTarget) q.push('quote_id=' + socialInt(SocialState.quoteTarget.id));
|
| 3406 |
+
data = await apiPost(`/api/social/posts?${q.join('&')}`);
|
| 3407 |
+
}
|
| 3408 |
+
if (data.post) {
|
| 3409 |
+
const wasEdit = !!SocialState.editingPost;
|
| 3410 |
+
closeModal(_el('social-compose-modal'));
|
| 3411 |
+
SocialState.editingPost = null;
|
| 3412 |
+
SocialState.modalMedia = []; SocialState.replyTarget = null; SocialState.quoteTarget = null;
|
| 3413 |
+
_el('social-compose-modal-input').value = '';
|
| 3414 |
+
_el('social-compose-modal-context').classList.add('hidden');
|
| 3415 |
+
socialPreviewModalMedia();
|
| 3416 |
+
showToast(wasEdit ? 'Post updated!' : 'Posted!', 'success');
|
| 3417 |
+
await SocialLoad.view(SocialState.view, true);
|
| 3418 |
+
}
|
| 3419 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3420 |
+
}
|
| 3421 |
+
|
| 3422 |
+
function openSocialEditModal(post) {
|
| 3423 |
+
if (!post) return;
|
| 3424 |
+
SocialState.editingPost = post;
|
| 3425 |
+
SocialState.modalMode = 'edit';
|
| 3426 |
+
SocialState.replyTarget = null;
|
| 3427 |
+
SocialState.quoteTarget = null;
|
| 3428 |
+
_el('social-compose-modal-title').innerHTML = '<i class="fas fa-pen"></i> Edit Post';
|
| 3429 |
+
_el('social-compose-modal-input').value = post.body || '';
|
| 3430 |
+
_el('social-compose-modal-context').classList.add('hidden');
|
| 3431 |
+
_el('social-compose-modal-media').value = '';
|
| 3432 |
+
SocialState.modalMedia = [];
|
| 3433 |
+
socialPreviewModalMedia();
|
| 3434 |
+
openModal(_el('social-compose-modal'));
|
| 3435 |
+
setTimeout(() => _el('social-compose-modal-input').focus(), 50);
|
| 3436 |
+
}
|
| 3437 |
+
|
| 3438 |
+
function copySocialPostLink(post) {
|
| 3439 |
+
const url = `${location.origin}/#social/${post.id}`;
|
| 3440 |
+
const done = () => showToast('Post link copied', 'success');
|
| 3441 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 3442 |
+
navigator.clipboard.writeText(url).then(done).catch(() => { prompt('Post link', url); done(); });
|
| 3443 |
+
} else {
|
| 3444 |
+
prompt('Post link', url);
|
| 3445 |
+
done();
|
| 3446 |
+
}
|
| 3447 |
+
}
|
| 3448 |
+
|
| 3449 |
+
|
| 3450 |
+
function openSocialLightbox(name, url) {
|
| 3451 |
+
if (!url) return;
|
| 3452 |
+
DOM.lightboxName.textContent = name || 'Media';
|
| 3453 |
+
DOM.lightboxImg.src = url;
|
| 3454 |
+
DOM.lightboxDownload.href = url;
|
| 3455 |
+
openModal(DOM.lightboxModal);
|
| 3456 |
+
}
|
| 3457 |
+
|
| 3458 |
+
function socialInt(v) { const n = Number(v); return Number.isFinite(n) ? Math.floor(n) : null; }
|
| 3459 |
+
|
| 3460 |
+
function openSocialComposeModal(mode, target) {
|
| 3461 |
+
SocialState.editingPost = null;
|
| 3462 |
+
SocialState.modalMode = mode || 'compose';
|
| 3463 |
+
SocialState.replyTarget = mode === 'reply' ? target : null;
|
| 3464 |
+
SocialState.quoteTarget = mode === 'quote' ? target : null;
|
| 3465 |
+
const context = _el('social-compose-modal-context');
|
| 3466 |
+
if (SocialState.replyTarget || SocialState.quoteTarget) {
|
| 3467 |
+
const post = SocialState.replyTarget || SocialState.quoteTarget;
|
| 3468 |
+
context.classList.remove('hidden');
|
| 3469 |
+
const label = SocialState.replyTarget ? 'Replying to' : 'Quoting';
|
| 3470 |
+
context.innerHTML = `${label} <strong>@${escapeHtml(post.author?.username || '')}</strong> — ${escapeHtml(truncate(post.body || '', 120))}`;
|
| 3471 |
+
} else context.classList.add('hidden');
|
| 3472 |
+
_el('social-compose-modal-title').innerHTML = mode === 'reply' ? '<i class="fas fa-reply"></i> Reply' : mode === 'quote' ? '<i class="fas fa-quote-right"></i> Quote' : '<i class="fas fa-feather"></i> Compose';
|
| 3473 |
+
openModal(_el('social-compose-modal'));
|
| 3474 |
+
setTimeout(() => _el('social-compose-modal-input').focus(), 50);
|
| 3475 |
+
}
|
| 3476 |
+
|
| 3477 |
+
function userCardHtml(u) {
|
| 3478 |
+
const following = !!u.is_following;
|
| 3479 |
+
return `<div class="social-user-card" data-username="${escapeHtml(u.username || '')}">
|
| 3480 |
+
<div class="user-avatar">${socialAvatarHtml(u)}</div>
|
| 3481 |
+
<div class="social-user-info"><strong>${escapeHtml(displayNameOf(u))}</strong><span>@${escapeHtml(u.username || '')}</span></div>
|
| 3482 |
+
<button class="btn btn-primary btn-sm social-follow-btn${following ? ' following' : ''}" data-follow="${escapeHtml(u.username || '')}">${following ? 'Following' : 'Follow'}</button>
|
| 3483 |
+
</div>`;
|
| 3484 |
+
}
|
| 3485 |
+
|
| 3486 |
+
async function toggleFollow(username, btn) {
|
| 3487 |
+
try {
|
| 3488 |
+
const data = await apiPost(`/api/social/users/${encodeURIComponent(username)}/follow`);
|
| 3489 |
+
if (data.following) { SocialState.following.add(username.toLowerCase()); }
|
| 3490 |
+
else { SocialState.following.delete(username.toLowerCase()); }
|
| 3491 |
+
document.querySelectorAll(`[data-follow="${CSS.escape(username)}"], [data-profile-follow="${CSS.escape(username)}"]`).forEach(b => {
|
| 3492 |
+
b.textContent = data.following ? 'Following' : 'Follow';
|
| 3493 |
+
b.classList.toggle('following', data.following);
|
| 3494 |
+
});
|
| 3495 |
+
await SocialLoad.suggestions();
|
| 3496 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3497 |
+
}
|
| 3498 |
+
|
| 3499 |
+
async function deleteSocialPost(id) {
|
| 3500 |
+
try {
|
| 3501 |
+
await apiDelete(`/api/social/posts/${id}`);
|
| 3502 |
+
const el = document.querySelector(`[data-post-id="${id}"]`);
|
| 3503 |
+
if (el) el.remove();
|
| 3504 |
+
showToast('Post deleted', 'success');
|
| 3505 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3506 |
+
}
|
| 3507 |
+
|
| 3508 |
+
async function socialAction(id, action) {
|
| 3509 |
+
try {
|
| 3510 |
+
if (action === 'delete') { await deleteSocialPost(id); return; }
|
| 3511 |
+
const url = `/api/social/posts/${id}/${action}`;
|
| 3512 |
+
const data = await apiPost(url);
|
| 3513 |
+
const post = SocialState.posts.get(Number(id));
|
| 3514 |
+
if (post) {
|
| 3515 |
+
if (action === 'like') { post.liked = data.liked; post.like_count = data.like_count; }
|
| 3516 |
+
if (action === 'repost') { post.reposted = data.reposted; post.repost_count = data.repost_count; }
|
| 3517 |
+
if (action === 'bookmark') { post.bookmarked = data.bookmarked; post.bookmark_count = data.bookmark_count || post.bookmark_count; }
|
| 3518 |
+
}
|
| 3519 |
+
const card = document.querySelector(`[data-post-id="${id}"]`);
|
| 3520 |
+
if (card) {
|
| 3521 |
+
const b = card.querySelector(`[data-action="${action}"]`);
|
| 3522 |
+
if (b) {
|
| 3523 |
+
if (action === 'like') b.classList.toggle('active', data.liked);
|
| 3524 |
+
if (action === 'repost') b.classList.toggle('active', data.reposted);
|
| 3525 |
+
if (action === 'bookmark') b.classList.toggle('active', data.bookmarked);
|
| 3526 |
+
const span = b.querySelector('span');
|
| 3527 |
+
if (span && action !== 'bookmark') span.textContent = socialCount(data.like_count != null ? data.like_count : data.repost_count);
|
| 3528 |
+
}
|
| 3529 |
+
}
|
| 3530 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3531 |
+
}
|
| 3532 |
+
|
| 3533 |
+
async function openProfile(username) {
|
| 3534 |
+
if (!username) return;
|
| 3535 |
+
try {
|
| 3536 |
+
SocialState.profileUser = { username };
|
| 3537 |
+
SocialState.isSearching = false;
|
| 3538 |
+
const prof = await apiGet(`/api/social/profile/${encodeURIComponent(username)}`);
|
| 3539 |
+
const posts = await apiGet(`/api/social/users/${encodeURIComponent(username)}/posts`);
|
| 3540 |
+
SocialState.profileUser = prof.profile;
|
| 3541 |
+
SocialState.posts.clear();
|
| 3542 |
+
(posts.posts || []).forEach(p => SocialState.posts.set(p.id, p));
|
| 3543 |
+
_el('social-compose').style.display = 'none';
|
| 3544 |
+
_el('social-page-title').textContent = '@' + username;
|
| 3545 |
+
_el('social-page-sub').textContent = displayNameOf(prof.profile) + "'s posts";
|
| 3546 |
+
document.querySelectorAll('.social-nav-link').forEach(l => l.classList.toggle('active', l.dataset.socialView === 'profile' && SocialState.user?.username === username));
|
| 3547 |
+
const p = prof.profile;
|
| 3548 |
+
const followBtn = p.is_self ? '' : `<button class="btn btn-primary btn-sm ${p.is_following ? 'following' : ''}" data-profile-follow="${escapeHtml(p.username)}">${p.is_following ? 'Following' : 'Follow'}</button>`;
|
| 3549 |
+
const header = `
|
| 3550 |
+
<section class="social-profile-card">
|
| 3551 |
+
<div class="social-profile-avatar">${p.avatar_path ? `<img src="${escapeHtml(fileUrl(p.avatar_path))}" alt="">` : `<span>${escapeHtml(initialOf(displayNameOf(p)))}</span>`}</div>
|
| 3552 |
+
<div class="social-profile-info">
|
| 3553 |
+
<div class="social-profile-name"><h2>${escapeHtml(displayNameOf(p))}</h2> <span class="text-secondary">@${escapeHtml(p.username)}</span> ${followBtn}</div>
|
| 3554 |
+
${p.bio ? `<p class="social-profile-bio">${linkifySocial(p.bio)}</p>` : ''}
|
| 3555 |
+
<div class="social-profile-meta">
|
| 3556 |
+
${p.location ? `<span><i class="fas fa-map-marker-alt"></i> ${escapeHtml(p.location)}</span>` : ''}
|
| 3557 |
+
${p.website ? `<a href="${escapeHtml(p.website)}" target="_blank" rel="noopener"><i class="fas fa-link"></i> ${escapeHtml(p.website.replace(/^https?:\/\//,''))}</a>` : ''}
|
| 3558 |
+
</div>
|
| 3559 |
+
<div class="social-profile-stats">
|
| 3560 |
+
<span><strong>${socialCount(p.posts_count || 0)}</strong> Posts</span>
|
| 3561 |
+
<span><strong>${socialCount(p.followers_count || 0)}</strong> Followers</span>
|
| 3562 |
+
<span><strong>${socialCount(p.following_count || 0)}</strong> Following</span>
|
| 3563 |
+
</div>
|
| 3564 |
+
</div>
|
| 3565 |
+
</section>`;
|
| 3566 |
+
renderSocialPosts([], _el('social-feed'));
|
| 3567 |
+
_el('social-feed').innerHTML = header + (posts.posts && posts.posts.length ? posts.posts.map(socialPostHtml).join('') : '<div class="social-feed-empty"><i class="fas fa-feather"></i><h3>No posts yet</h3></div>');
|
| 3568 |
+
_el('social-search-input').value = '';
|
| 3569 |
+
_el('social-search-clear').classList.add('hidden');
|
| 3570 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3571 |
+
}
|
| 3572 |
+
|
| 3573 |
+
const SocialLoad = {
|
| 3574 |
+
async view(name, force) {
|
| 3575 |
+
SocialState.isSearching = false;
|
| 3576 |
+
SocialState.profileUser = null;
|
| 3577 |
+
SocialState.view = name;
|
| 3578 |
+
document.querySelectorAll('.social-nav-link').forEach(l => l.classList.toggle('active', l.dataset.socialView === name));
|
| 3579 |
+
const titles = { home: ['Home', 'Posts from people you follow'], explore: ['Explore', 'Discover what is happening'], notifications: ['Notifications', 'Your social activity'], bookmarks: ['Bookmarks', 'Your saved posts'], profile: ['My Profile', 'Your public profile'] };
|
| 3580 |
+
const t = titles[name] || ['Home', ''];
|
| 3581 |
+
_el('social-page-title').textContent = t[0]; _el('social-page-sub').textContent = t[1];
|
| 3582 |
+
_el('social-compose').style.display = (name === 'home' || name === 'explore') ? '' : 'none';
|
| 3583 |
+
_el('social-search-input').value = '';
|
| 3584 |
+
_el('social-search-clear').classList.add('hidden');
|
| 3585 |
+
if (name === 'notifications') { await this.notifications(); return; }
|
| 3586 |
+
if (name === 'bookmarks') { await this.bookmarks(); return; }
|
| 3587 |
+
if (name === 'profile') { await openProfile(State.user.username); return; }
|
| 3588 |
+
const feed = name === 'explore' ? 'explore' : 'home';
|
| 3589 |
+
await this.feed(feed, force);
|
| 3590 |
+
},
|
| 3591 |
+
async feed(feed, force) {
|
| 3592 |
+
const loading = _el('social-loading');
|
| 3593 |
+
loading.classList.remove('hidden');
|
| 3594 |
+
try {
|
| 3595 |
+
const data = await apiGet(`/api/social/feed?feed=${feed}&limit=50`);
|
| 3596 |
+
if (!force) SocialState.posts.clear();
|
| 3597 |
+
let posts = data.posts || [];
|
| 3598 |
+
if (feed === 'home' && SocialState.settings.hideReposts) posts = posts.filter(p => !p.reposter_id);
|
| 3599 |
+
renderSocialPosts(posts);
|
| 3600 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3601 |
+
finally { loading.classList.add('hidden'); }
|
| 3602 |
+
},
|
| 3603 |
+
async notifications() {
|
| 3604 |
+
try {
|
| 3605 |
+
const data = await apiGet(`/api/social/notifications`);
|
| 3606 |
+
const list = data.notifications || [];
|
| 3607 |
+
if (!list.length) { renderSocialPosts([], _el('social-feed')); return; }
|
| 3608 |
+
const unread = list.filter(n => !n.read).length;
|
| 3609 |
+
_el('social-unread-badge').textContent = unread;
|
| 3610 |
+
_el('social-unread-badge').classList.toggle('hidden', !unread || !SocialState.settings.badges);
|
| 3611 |
+
_el('social-feed').innerHTML = list.map(n => `
|
| 3612 |
+
<article class="social-notification" data-post-id="${n.post_id || ''}">
|
| 3613 |
+
<div class="social-avatar ah">${socialAvatarHtml(n.actor, 'social-avatar-img')}</div>
|
| 3614 |
+
<div class="social-post-main">
|
| 3615 |
+
<div class="social-notification-body"><strong>${escapeHtml(displayNameOf(n.actor))}</strong>
|
| 3616 |
+
<span class="social-username">@${escapeHtml(n.actor.username || '')}</span> ${escapeHtml(n.type)}d</div>
|
| 3617 |
+
<span class="social-time">${socialTime(n.created_at_ms)}</span>
|
| 3618 |
+
</div>
|
| 3619 |
+
</article>`).join('');
|
| 3620 |
+
await apiPost(`/api/social/notifications/read`);
|
| 3621 |
+
_el('social-unread-badge').classList.add('hidden');
|
| 3622 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3623 |
+
},
|
| 3624 |
+
async bookmarks() {
|
| 3625 |
+
try {
|
| 3626 |
+
const data = await apiGet(`/api/social/bookmarks`);
|
| 3627 |
+
renderSocialPosts(data.posts || []);
|
| 3628 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3629 |
+
},
|
| 3630 |
+
async trending() {
|
| 3631 |
+
try {
|
| 3632 |
+
const data = await apiGet(`/api/social/trending`);
|
| 3633 |
+
const box = _el('social-trending-list');
|
| 3634 |
+
if (!data.trending || !data.trending.length) { box.innerHTML = '<div class="text-secondary">No trends yet</div>'; return; }
|
| 3635 |
+
box.innerHTML = data.trending.slice(0, 8).map(t => `<button class="social-trend-item" data-hashtag="${encodeURIComponent(t.tag)}"><span class="social-trend-rank">${'#'}</span><span>#${escapeHtml(t.tag)}</span><span class="social-trend-count">${socialCount(t.count)} posts</span></button>`).join('');
|
| 3636 |
+
} catch (e) { /* non-fatal */ }
|
| 3637 |
+
},
|
| 3638 |
+
async suggestions() {
|
| 3639 |
+
try {
|
| 3640 |
+
const data = await apiGet(`/api/social/suggestions?limit=5`);
|
| 3641 |
+
const box = _el('social-suggest-list');
|
| 3642 |
+
if (!data.users || !data.users.length) { box.innerHTML = '<div class="text-secondary">You are all caught up</div>'; return; }
|
| 3643 |
+
box.innerHTML = data.users.map(userCardHtml).join('');
|
| 3644 |
+
} catch (e) { box.innerHTML = '<div class="text-secondary">Could not load suggestions</div>'; }
|
| 3645 |
+
}
|
| 3646 |
+
};
|
| 3647 |
+
|
| 3648 |
+
async function socialSearch(q) {
|
| 3649 |
+
q = (q || '').trim();
|
| 3650 |
+
_el('social-search-clear').classList.toggle('hidden', !q);
|
| 3651 |
+
if (!q) { await SocialLoad.view(SocialState.view || 'home'); return; }
|
| 3652 |
+
SocialState.isSearching = true;
|
| 3653 |
+
_el('social-compose').style.display = 'none';
|
| 3654 |
+
_el('social-page-title').textContent = 'Search';
|
| 3655 |
+
_el('social-page-sub').textContent = 'Results for "' + q + '"';
|
| 3656 |
+
try {
|
| 3657 |
+
const data = await apiGet(`/api/social/search?q=${encodeURIComponent(q)}`);
|
| 3658 |
+
(data.posts || []).forEach(p => SocialState.posts.set(p.id, p));
|
| 3659 |
+
const users = (data.users || []).map(userCardHtml).join('');
|
| 3660 |
+
const posts = (data.posts || []).map(socialPostHtml).join('');
|
| 3661 |
+
_el('social-feed').innerHTML = (users ? `<section class="social-search-users"><h3>People</h3>${users}</section>` : '') + (posts ? `<section class="social-search-posts"><h3>Posts</h3>${posts}</section>` : (!users && !posts ? '<div class="social-feed-empty"><i class="fas fa-search"></i><h3>No results</h3></div>' : ''));
|
| 3662 |
+
} catch (e) { showToast(e.message, 'error'); }
|
| 3663 |
+
}
|
| 3664 |
+
|
| 3665 |
+
function initSocial() {
|
| 3666 |
+
if (SocialState.inited) return;
|
| 3667 |
+
SocialState.inited = true;
|
| 3668 |
+
updateSocialMe();
|
| 3669 |
+
const sc = _el('social-screen');
|
| 3670 |
+
if (!sc) return;
|
| 3671 |
+
_el('social-back-btn').addEventListener('click', backToChat);
|
| 3672 |
+
_el('social-btn')?.addEventListener('click', openSocial);
|
| 3673 |
+
_el('social-compose-nav-btn')?.addEventListener('click', () => openSocialComposeModal());
|
| 3674 |
+
_el('social-post-btn')?.addEventListener('click', submitSocialPost);
|
| 3675 |
+
document.querySelectorAll('.social-nav-link').forEach(l => l.addEventListener('click', () => SocialLoad.view(l.dataset.socialView)));
|
| 3676 |
+
_el('social-media-input')?.addEventListener('change', e => addSocialMedia(e.target.files, false));
|
| 3677 |
+
_el('social-compose-modal-media')?.addEventListener('change', e => addSocialMedia(e.target.files, true));
|
| 3678 |
+
_el('social-compose-modal-submit')?.addEventListener('click', submitModalSocialPost);
|
| 3679 |
+
_el('social-search-input')?.addEventListener('input', debounce(e => socialSearch(e.target.value), 350));
|
| 3680 |
+
_el('social-search-clear')?.addEventListener('click', () => { _el('social-search-input').value=''; socialSearch(''); });
|
| 3681 |
+
_el('social-settings-btn')?.addEventListener('click', openSocialSettings);
|
| 3682 |
+
_el('social-settings-save')?.addEventListener('click', closeSocialSettingsAndSave);
|
| 3683 |
+
_el('social-post-input')?.addEventListener('input', () => { _el('social-post-btn').disabled = !_el('social-post-input').value.trim() && !SocialState.media.length; _el('social-post-count').textContent = _el('social-post-input').value.length ? `${_el('social-post-input').value.length}/28000` : ''; });
|
| 3684 |
+
_el('social-feed')?.addEventListener('click', async (e) => {
|
| 3685 |
+
const fbtn = e.target.closest('[data-follow]'), pbtn = e.target.closest('[data-profile-follow]');
|
| 3686 |
+
if (fbtn) { await toggleFollow(fbtn.dataset.follow, fbtn); return; }
|
| 3687 |
+
if (pbtn) { await toggleFollow(pbtn.dataset.profileFollow, pbtn); return; }
|
| 3688 |
+
const profile = e.target.closest('[data-username]');
|
| 3689 |
+
if (profile && !e.target.closest('.sp-action') && !e.target.closest('.social-follow-btn')) { openProfile(profile.dataset.username); return; }
|
| 3690 |
+
const mention = e.target.closest('[data-username]');
|
| 3691 |
+
if (mention) { openProfile(mention.dataset.username); return; }
|
| 3692 |
+
const hashtag = e.target.closest('[data-hashtag]');
|
| 3693 |
+
if (hashtag) { socialSearch('#' + decodeURIComponent(hashtag.dataset.hashtag)); _el('social-search-input').value = '#' + decodeURIComponent(hashtag.dataset.hashtag); return; }
|
| 3694 |
+
const mediaImg = e.target.closest('[data-media-image]');
|
| 3695 |
+
if (mediaImg && e.target.tagName.toLowerCase() === 'img') { openSocialLightbox(mediaImg.dataset.mediaName, mediaImg.dataset.mediaImage); return; }
|
| 3696 |
+
const actionBtn = e.target.closest('[data-action]');
|
| 3697 |
+
if (actionBtn) {
|
| 3698 |
+
const id = Number(actionBtn.dataset.id), action = actionBtn.dataset.action;
|
| 3699 |
+
if (action === 'reply') { const post = SocialState.posts.get(id); openSocialComposeModal('reply', post); return; }
|
| 3700 |
+
if (action === 'quote') { const post = SocialState.posts.get(id); openSocialComposeModal('quote', post); return; }
|
| 3701 |
+
if (action === 'edit') { const post = SocialState.posts.get(id); openSocialEditModal(post); return; }
|
| 3702 |
+
if (action === 'share') { const post = SocialState.posts.get(id); copySocialPostLink(post); return; }
|
| 3703 |
+
if (action === 'profile') { const un = actionBtn.dataset.username; openProfile(un); return; }
|
| 3704 |
+
await socialAction(id, action); return;
|
| 3705 |
+
}
|
| 3706 |
+
const notif = e.target.closest('.social-notification');
|
| 3707 |
+
if (notif && notif.dataset.postId) {
|
| 3708 |
+
try { const data = await apiGet(`/api/social/posts/${notif.dataset.postId}`); SocialState.posts.set(data.post.id, data.post); renderSocialPosts([data.post]); _el('social-compose').style.display='none'; }
|
| 3709 |
+
catch (e2) { showToast(e2.message, 'error'); }
|
| 3710 |
+
}
|
| 3711 |
+
});
|
| 3712 |
+
_el('social-media-preview')?.addEventListener('click', (e) => { const b = e.target.closest('[data-remove-media]'); if (b) { SocialState.media.splice(Number(b.dataset.removeMedia), 1); socialPreviewMedia(); } });
|
| 3713 |
+
_el('social-compose-modal-media-preview')?.addEventListener('click', (e) => { const b = e.target.closest('[data-remove-modal-media]'); if (b) { SocialState.modalMedia.splice(Number(b.dataset.removeModalMedia), 1); socialPreviewModalMedia(); } });
|
| 3714 |
+
_el('social-logout-btn')?.addEventListener('click', () => { const logoutBtn = document.getElementById('logout-btn'); if (logoutBtn) logoutBtn.click(); });
|
| 3715 |
+
_el('social-feed')?.classList.toggle('social-feed-compact', !!SocialState.settings.compact);
|
| 3716 |
+
_el('social-unread-badge')?.classList.toggle('hidden', !SocialState.settings.badges);
|
| 3717 |
+
if (location.hash.startsWith('#social/')) {
|
| 3718 |
+
const id = Number(location.hash.split('/').pop());
|
| 3719 |
+
if (id) {
|
| 3720 |
+
apiGet(`/api/social/posts/${id}`).then(d => {
|
| 3721 |
+
SocialState.posts.set(d.post.id, d.post);
|
| 3722 |
+
renderSocialPosts([d.post]);
|
| 3723 |
+
_el('social-compose').style.display = 'none';
|
| 3724 |
+
}).catch(() => {});
|
| 3725 |
+
}
|
| 3726 |
+
}
|
| 3727 |
+
SocialLoad.trending();
|
| 3728 |
+
SocialLoad.suggestions();
|
| 3729 |
+
}
|
| 3730 |
+
|
| 3731 |
+
function openSocialSettings() {
|
| 3732 |
+
const s = SocialState.settings;
|
| 3733 |
+
_el('social-setting-autoplay').checked = !!s.autoplay;
|
| 3734 |
+
_el('social-setting-muted').checked = !!s.muted;
|
| 3735 |
+
_el('social-setting-hide-reposts').checked = !!s.hideReposts;
|
| 3736 |
+
_el('social-setting-compact').checked = !!s.compact;
|
| 3737 |
+
_el('social-setting-live').checked = !!s.live;
|
| 3738 |
+
_el('social-setting-badges').checked = !!s.badges;
|
| 3739 |
+
_el('social-setting-default').value = s.defaultFeed || 'home';
|
| 3740 |
+
openModal(_el('social-settings-modal'));
|
| 3741 |
+
}
|
| 3742 |
+
|
| 3743 |
+
function applySocialSettings() {
|
| 3744 |
+
const s = SocialState.settings;
|
| 3745 |
+
saveSocialSettings();
|
| 3746 |
+
const feed = _el('social-feed');
|
| 3747 |
+
if (feed) feed.classList.toggle('social-feed-compact', !!s.compact);
|
| 3748 |
+
_el('social-unread-badge').classList.toggle('hidden', !s.badges);
|
| 3749 |
+
if (_el('social-screen').classList.contains('active')) {
|
| 3750 |
+
if (s.live) startSocialPoll(); else stopSocialPoll();
|
| 3751 |
+
SocialLoad.view(SocialState.view, true);
|
| 3752 |
+
}
|
| 3753 |
+
}
|
| 3754 |
+
|
| 3755 |
+
function closeSocialSettingsAndSave() {
|
| 3756 |
+
const s = SocialState.settings;
|
| 3757 |
+
s.autoplay = _el('social-setting-autoplay').checked;
|
| 3758 |
+
s.muted = _el('social-setting-muted').checked;
|
| 3759 |
+
s.hideReposts = _el('social-setting-hide-reposts').checked;
|
| 3760 |
+
s.compact = _el('social-setting-compact').checked;
|
| 3761 |
+
s.live = _el('social-setting-live').checked;
|
| 3762 |
+
s.badges = _el('social-setting-badges').checked;
|
| 3763 |
+
s.defaultFeed = _el('social-setting-default').value;
|
| 3764 |
+
applySocialSettings();
|
| 3765 |
+
closeModal(_el('social-settings-modal'));
|
| 3766 |
+
showToast('Social settings saved', 'success');
|
| 3767 |
+
}
|
| 3768 |
+
|
| 3769 |
+
|
| 3770 |
async function init() {
|
| 3771 |
applyPrefs();
|
| 3772 |
DOM.aboutVersion.textContent = APP_VERSION;
|
|
|
|
| 3778 |
State.user = data.user;
|
| 3779 |
showChat();
|
| 3780 |
connectWebSocket();
|
| 3781 |
+
initSocial();
|
| 3782 |
return;
|
| 3783 |
}
|
| 3784 |
} catch (e) { /* fall through to auth screen */ }
|
|
|
|
| 3796 |
|
| 3797 |
function showChat() {
|
| 3798 |
DOM.authScreen.classList.remove('active');
|
| 3799 |
+
const sc = document.getElementById('social-screen');
|
| 3800 |
+
if (sc) sc.classList.remove('active');
|
| 3801 |
DOM.chatScreen.classList.add('active');
|
| 3802 |
updateSidebarMe();
|
| 3803 |
}
|