avatar fix
This commit is contained in:
@ -15,3 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Logged-in chrome matches TTP: navy header, Font Awesome 6.7.1 / Bootstrap 5.3, centered full-width search, notifications and messages dropdowns, avatar account menu. Profile settings (avatar, name, gender, newsletter, timezone, date/time, page size, dark mode) live in-app and save through `POST /api/profile/update`. Email, password, and phone open the connected site.
|
- Logged-in chrome matches TTP: navy header, Font Awesome 6.7.1 / Bootstrap 5.3, centered full-width search, notifications and messages dropdowns, avatar account menu. Profile settings (avatar, name, gender, newsletter, timezone, date/time, page size, dark mode) live in-app and save through `POST /api/profile/update`. Email, password, and phone open the connected site.
|
||||||
- After sign-in, Capsule loads `GET /api/profile` plus notifications, the messages inbox, and `GET /api/messages/recent` for the header dropdown (avatars + unread count). Inbox rows can mark read/unread or hide. Compose and reply follow `canSend`. Search uses the matching user-token endpoint. Contact and bug reports live on footer pages (`#/contact`, `#/bugreport`) instead of the dashboard. Disabled plugins show an unavailable note instead of demo data.
|
- After sign-in, Capsule loads `GET /api/profile` plus notifications, the messages inbox, and `GET /api/messages/recent` for the header dropdown (avatars + unread count). Inbox rows can mark read/unread or hide. Compose and reply follow `canSend`. Search uses the matching user-token endpoint. Contact and bug reports live on footer pages (`#/contact`, `#/bugreport`) instead of the dashboard. Disabled plugins show an unavailable note instead of demo data.
|
||||||
- Footer chrome matches TTP copy and socials. The upper band keeps a dark-mode toggle, Privacy Policy, Terms of Service, Contact, and Report a Bug. There is no subscribe box.
|
- Footer chrome matches TTP copy and socials. The upper band keeps a dark-mode toggle, Privacy Policy, Terms of Service, Contact, and Report a Bug. There is no subscribe box.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Avatar file previews are allowed (`blob:` on `img-src`). Choosing a photo no longer shows the missing-image icon. A `{ "error": "not found" }` from the site is reported as a missing Capsule API action (the live TTP install still needs `POST /api/profile/update`).
|
||||||
|
|||||||
@ -140,6 +140,8 @@ export function apiErrorMessage(code, errors) {
|
|||||||
return 'Choose how you want to authenticate.'
|
return 'Choose how you want to authenticate.'
|
||||||
case 'user token required':
|
case 'user token required':
|
||||||
return 'This action needs a personal (user) token, not an app token.'
|
return 'This action needs a personal (user) token, not an app token.'
|
||||||
|
case 'not found':
|
||||||
|
return 'This site is missing that Capsule API action. Pull the latest TTP on the server.'
|
||||||
default:
|
default:
|
||||||
return code || 'The site returned an error.'
|
return code || 'The site returned an error.'
|
||||||
}
|
}
|
||||||
@ -338,21 +340,55 @@ export function unwrapApi(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blob for an avatar sent over IPC.
|
* Bytes for an avatar sent over IPC.
|
||||||
|
*
|
||||||
|
* @param {unknown} data - ArrayBuffer, typed array, Buffer, or serialized Buffer
|
||||||
|
* @return {Buffer|null}
|
||||||
|
*/
|
||||||
|
function avatarBytes(data) {
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (Buffer.isBuffer(data)) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
return Buffer.from(data)
|
||||||
|
}
|
||||||
|
if (ArrayBuffer.isView(data)) {
|
||||||
|
return Buffer.from(data.buffer, data.byteOffset, data.byteLength)
|
||||||
|
}
|
||||||
|
if (data.type === 'Buffer' && Array.isArray(data.data)) {
|
||||||
|
return Buffer.from(data.data)
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return Buffer.from(data)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const bytes = Buffer.from(data)
|
||||||
|
return bytes.length ? bytes : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File/Blob for an avatar sent over IPC.
|
||||||
*
|
*
|
||||||
* @param {object} [avatar] - name, type, data (ArrayBuffer or typed array)
|
* @param {object} [avatar] - name, type, data (ArrayBuffer or typed array)
|
||||||
* @return {Blob|null}
|
* @return {Blob|null}
|
||||||
*/
|
*/
|
||||||
export function avatarBlob(avatar) {
|
export function avatarBlob(avatar) {
|
||||||
if (!avatar?.data) {
|
const bytes = avatarBytes(avatar?.data)
|
||||||
|
if (!bytes) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const bytes = Buffer.isBuffer(avatar.data)
|
const type = avatar.type || 'application/octet-stream'
|
||||||
? avatar.data
|
const name = avatar.name || 'avatar.jpg'
|
||||||
: avatar.data instanceof ArrayBuffer
|
if (typeof File === 'function') {
|
||||||
? Buffer.from(avatar.data)
|
return new File([bytes], name, { type })
|
||||||
: Buffer.from(avatar.data)
|
}
|
||||||
return new Blob([bytes], { type: avatar.type || 'application/octet-stream' })
|
return new Blob([bytes], { type })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -386,7 +422,11 @@ export async function updateProfile(siteUrl, token, fields, avatar) {
|
|||||||
}
|
}
|
||||||
data.append(key, value)
|
data.append(key, value)
|
||||||
})
|
})
|
||||||
|
if (typeof File === 'function' && file instanceof File) {
|
||||||
|
data.append('avatar', file)
|
||||||
|
} else {
|
||||||
data.append('avatar', file, avatar.name || 'avatar.jpg')
|
data.append('avatar', file, avatar.name || 'avatar.jpg')
|
||||||
|
}
|
||||||
return ttpRequest({
|
return ttpRequest({
|
||||||
siteUrl,
|
siteUrl,
|
||||||
path: '/api/profile/update',
|
path: '/api/profile/update',
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; style-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; font-src https://cdnjs.cloudflare.com; script-src 'self' https://cdn.jsdelivr.net; img-src 'self' data: https: http:;"
|
content="default-src 'self'; style-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; font-src https://cdnjs.cloudflare.com; script-src 'self' https://cdn.jsdelivr.net; img-src 'self' data: blob: https: http:;"
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Capsule</title>
|
<title>Capsule</title>
|
||||||
|
|||||||
@ -52,9 +52,12 @@ let profile = { ...demoProfile }
|
|||||||
let plugins = []
|
let plugins = []
|
||||||
let notifications = { items: [], unread: 0 }
|
let notifications = { items: [], unread: 0 }
|
||||||
let messages = { items: [], unread: 0 }
|
let messages = { items: [], unread: 0 }
|
||||||
|
let recentMessages = null
|
||||||
|
let canSendMessages = true
|
||||||
let thread = null
|
let thread = null
|
||||||
let searchState = { items: [], q: '', resource: 'all', resources: [], page: 1, pages: 0, total: 0 }
|
let searchState = { items: [], q: '', resource: 'all', resources: [], page: 1, pages: 0, total: 0 }
|
||||||
let pendingAvatar = null
|
let pendingAvatar = null
|
||||||
|
let pendingAvatarPreview = null
|
||||||
let workspaceSeq = 0
|
let workspaceSeq = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -156,17 +159,47 @@ function displaySiteName() {
|
|||||||
* @return {string} - image URL
|
* @return {string} - image URL
|
||||||
*/
|
*/
|
||||||
function avatarUrl() {
|
function avatarUrl() {
|
||||||
if (profile.avatarUrl) {
|
if (pendingAvatarPreview) {
|
||||||
return profile.avatarUrl
|
return pendingAvatarPreview
|
||||||
}
|
}
|
||||||
if (!session?.siteUrl) {
|
return mediaUrl(profile.avatarUrl || profile.avatar || 'images/defaultAvatar.png')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute URL for a site path or already-absolute avatar.
|
||||||
|
*
|
||||||
|
* @param {string} pathOrUrl - stored path or URL
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
function mediaUrl(pathOrUrl) {
|
||||||
|
const value = String(pathOrUrl || '')
|
||||||
|
.replace(/\{BASE\}/g, '')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^\/+/, '')
|
||||||
|
if (!value) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
const path = profile.avatar || 'images/defaultAvatar.png'
|
if (/^https?:\/\//i.test(value)) {
|
||||||
if (/^https?:\/\//i.test(path)) {
|
return value
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
return `${session.siteUrl.replace(/\/+$/, '')}/${String(path).replace(/^\/+/, '')}`
|
const root = session?.siteUrl || session?.lastSiteUrl
|
||||||
|
if (!root) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return `${root.replace(/\/+$/, '')}/${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a chosen avatar file and its preview URL.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
function clearPendingAvatar() {
|
||||||
|
if (pendingAvatarPreview) {
|
||||||
|
URL.revokeObjectURL(pendingAvatarPreview)
|
||||||
|
}
|
||||||
|
pendingAvatarPreview = null
|
||||||
|
pendingAvatar = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -319,6 +352,10 @@ function renderChrome() {
|
|||||||
avatar.hidden = true
|
avatar.hidden = true
|
||||||
avatarFallback.hidden = false
|
avatarFallback.hidden = false
|
||||||
}
|
}
|
||||||
|
avatar.onload = () => {
|
||||||
|
avatar.hidden = false
|
||||||
|
avatarFallback.hidden = true
|
||||||
|
}
|
||||||
|
|
||||||
logo.hidden = !nextLogo
|
logo.hidden = !nextLogo
|
||||||
if (nextLogo) {
|
if (nextLogo) {
|
||||||
@ -344,9 +381,10 @@ function renderChrome() {
|
|||||||
document.getElementById('settings-link-phone').href = phone
|
document.getElementById('settings-link-phone').href = phone
|
||||||
|
|
||||||
const noteItems = notifications.items || []
|
const noteItems = notifications.items || []
|
||||||
const mailItems = messages.items || []
|
const mailMenu = dropdownMail()
|
||||||
|
const mailItems = mailMenu.items || []
|
||||||
const unreadNotes = Number(notifications.unread ?? noteItems.filter((item) => item.unread).length)
|
const unreadNotes = Number(notifications.unread ?? noteItems.filter((item) => item.unread).length)
|
||||||
const unreadMail = Number(messages.unread ?? mailItems.filter((item) => item.unread).length)
|
const unreadMail = Number(mailMenu.unread ?? messages.unread ?? mailItems.filter((item) => item.unread).length)
|
||||||
const noteBadge = document.getElementById('notification-badge')
|
const noteBadge = document.getElementById('notification-badge')
|
||||||
const mailBadge = document.getElementById('message-badge')
|
const mailBadge = document.getElementById('message-badge')
|
||||||
noteBadge.hidden = unreadNotes === 0
|
noteBadge.hidden = unreadNotes === 0
|
||||||
@ -356,7 +394,8 @@ function renderChrome() {
|
|||||||
|
|
||||||
fillDropdown(
|
fillDropdown(
|
||||||
document.getElementById('notification-menu'),
|
document.getElementById('notification-menu'),
|
||||||
noteItems
|
noteItems.length
|
||||||
|
? noteItems
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map(
|
.map(
|
||||||
(item) => `
|
(item) => `
|
||||||
@ -368,23 +407,36 @@ function renderChrome() {
|
|||||||
</li>`
|
</li>`
|
||||||
)
|
)
|
||||||
.join('')
|
.join('')
|
||||||
|
: `<li data-demo-item class="px-3 text-center"><strong>No Notifications</strong></li>`
|
||||||
)
|
)
|
||||||
|
|
||||||
fillDropdown(
|
fillDropdown(
|
||||||
document.getElementById('message-menu'),
|
document.getElementById('message-menu'),
|
||||||
mailItems
|
mailItems.length
|
||||||
|
? mailItems
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map(
|
.map((item) => {
|
||||||
(item) => `
|
const name = escapeHtml(item.otherUserPretty || item.otherUser || '')
|
||||||
|
const src = escapeHtml(item.otherAvatarUrl || mediaUrl(item.otherAvatar || ''))
|
||||||
|
const img = src
|
||||||
|
? `<img class="capsule-drop-avatar" src="${src}" alt="" width="40" height="40" />`
|
||||||
|
: ''
|
||||||
|
return `
|
||||||
<li data-demo-item>
|
<li data-demo-item>
|
||||||
<a href="#/messages/${encodeURIComponent(item.id)}" class="dropdown-item dropdown-item-block">
|
<a href="#/messages/${encodeURIComponent(item.id)}" class="dropdown-item dropdown-item-block">
|
||||||
<strong>${escapeHtml(item.otherUser || '')}</strong>
|
<div class="d-flex gap-2">
|
||||||
|
${img}
|
||||||
|
<div>
|
||||||
|
<strong>${name}</strong>
|
||||||
<p class="small text-muted mb-1"><i class="fa fa-fw fa-clock me-1"></i>${escapeHtml(formatTime(item.lastMessageAt))}</p>
|
<p class="small text-muted mb-1"><i class="fa fa-fw fa-clock me-1"></i>${escapeHtml(formatTime(item.lastMessageAt))}</p>
|
||||||
<span>${escapeHtml(item.preview || '')}</span>
|
<span>${escapeHtml(item.preview || '')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>`
|
</li>`
|
||||||
)
|
})
|
||||||
.join('')
|
.join('')
|
||||||
|
: `<li data-demo-item class="px-3 text-center"><strong>No Messages</strong></li>`
|
||||||
)
|
)
|
||||||
|
|
||||||
renderFooter()
|
renderFooter()
|
||||||
@ -430,6 +482,30 @@ function fillDropdown(list, html) {
|
|||||||
list.insertAdjacentHTML('afterbegin', html)
|
list.insertAdjacentHTML('afterbegin', html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recent conversations for the header menu, or the inbox page if recent is missing.
|
||||||
|
*
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
function dropdownMail() {
|
||||||
|
if (recentMessages && !recentMessages.unavailable) {
|
||||||
|
return recentMessages
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply canSend from an API payload when present.
|
||||||
|
*
|
||||||
|
* @param {object} [payload] - messages JSON
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
function applyCanSend(payload) {
|
||||||
|
if (payload && typeof payload.canSend === 'boolean') {
|
||||||
|
canSendMessages = payload.canSend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paint the MFA picker or code form from a pending challenge.
|
* Paint the MFA picker or code form from a pending challenge.
|
||||||
*
|
*
|
||||||
@ -549,17 +625,33 @@ function renderPages() {
|
|||||||
} else {
|
} else {
|
||||||
mailEmpty.hidden = true
|
mailEmpty.hidden = true
|
||||||
document.getElementById('message-list').innerHTML = messages.items
|
document.getElementById('message-list').innerHTML = messages.items
|
||||||
.map(
|
.map((item) => {
|
||||||
(item) => `
|
const name = escapeHtml(item.otherUserPretty || item.otherUser || '')
|
||||||
<tr class="capsule-row-link ${item.unread ? 'is-unread' : ''}" data-message-id="${escapeHtml(item.id)}">
|
const id = escapeHtml(item.id)
|
||||||
<td>${escapeHtml(item.otherUser || '')}</td>
|
return `
|
||||||
|
<tr class="capsule-row-link ${item.unread ? 'is-unread' : ''}" data-message-id="${id}">
|
||||||
|
<td>${name}</td>
|
||||||
<td>${escapeHtml(item.preview || '')}</td>
|
<td>${escapeHtml(item.preview || '')}</td>
|
||||||
<td class="text-muted">${escapeHtml(formatTime(item.lastMessageAt))}</td>
|
<td class="text-muted">${escapeHtml(formatTime(item.lastMessageAt))}</td>
|
||||||
|
<td class="text-nowrap" data-message-actions>
|
||||||
|
<button type="button" class="btn btn-sm btn-info" data-message-read="${id}" title="Mark as read" ${item.unread ? '' : 'disabled'}>
|
||||||
|
<i class="fa-solid fa-fw fa-envelope-open"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-message-unread="${id}" title="Mark as unread" ${item.unread ? 'disabled' : ''}>
|
||||||
|
<i class="fa-solid fa-fw fa-envelope"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" data-message-delete="${id}" title="Hide">
|
||||||
|
<i class="fa-solid fa-fw fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>`
|
</tr>`
|
||||||
)
|
})
|
||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('messages-new').hidden = !canSendMessages || Boolean(messages.unavailable)
|
||||||
|
document.getElementById('thread-reply').hidden = !canSendMessages
|
||||||
|
|
||||||
const displayName = profile.name || profile.username || session.username || ''
|
const displayName = profile.name || profile.username || session.username || ''
|
||||||
document.getElementById('profile-name').textContent = displayName
|
document.getElementById('profile-name').textContent = displayName
|
||||||
document.getElementById('profile-registered').textContent = profile.registered || ''
|
document.getElementById('profile-registered').textContent = profile.registered || ''
|
||||||
@ -840,9 +932,11 @@ function applySession(next) {
|
|||||||
plugins = []
|
plugins = []
|
||||||
notifications = { items: [], unread: 0 }
|
notifications = { items: [], unread: 0 }
|
||||||
messages = { items: [], unread: 0 }
|
messages = { items: [], unread: 0 }
|
||||||
|
recentMessages = null
|
||||||
|
canSendMessages = true
|
||||||
thread = null
|
thread = null
|
||||||
profile = { ...demoProfile }
|
profile = { ...demoProfile }
|
||||||
pendingAvatar = null
|
clearPendingAvatar()
|
||||||
} else if (!window.location.hash) {
|
} else if (!window.location.hash) {
|
||||||
window.location.hash = '#/'
|
window.location.hash = '#/'
|
||||||
}
|
}
|
||||||
@ -856,6 +950,8 @@ function applySession(next) {
|
|||||||
if (session?.preview) {
|
if (session?.preview) {
|
||||||
notifications = { items: demoNotifications, unread: demoNotifications.filter((item) => item.unread).length }
|
notifications = { items: demoNotifications, unread: demoNotifications.filter((item) => item.unread).length }
|
||||||
messages = { items: demoMessages, unread: demoMessages.filter((item) => item.unread).length }
|
messages = { items: demoMessages, unread: demoMessages.filter((item) => item.unread).length }
|
||||||
|
recentMessages = { items: demoMessages, unread: messages.unread }
|
||||||
|
canSendMessages = true
|
||||||
profile = { ...demoProfile }
|
profile = { ...demoProfile }
|
||||||
plugins = []
|
plugins = []
|
||||||
} else if (session?.connected && window.capsule) {
|
} else if (session?.connected && window.capsule) {
|
||||||
@ -885,6 +981,9 @@ async function loadWorkspace() {
|
|||||||
applyProfile(data.profile)
|
applyProfile(data.profile)
|
||||||
notifications = data.notifications || { items: [], unread: 0 }
|
notifications = data.notifications || { items: [], unread: 0 }
|
||||||
messages = data.messages || { items: [], unread: 0 }
|
messages = data.messages || { items: [], unread: 0 }
|
||||||
|
recentMessages = data.recentMessages || null
|
||||||
|
applyCanSend(messages)
|
||||||
|
applyCanSend(recentMessages)
|
||||||
renderChrome()
|
renderChrome()
|
||||||
renderPages()
|
renderPages()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -909,16 +1008,42 @@ async function loadThread(id) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
thread = data
|
thread = data
|
||||||
|
applyCanSend(data)
|
||||||
renderThread()
|
renderThread()
|
||||||
messages = (await runApi(() => window.capsule.messages({ page: 1 }))) || messages
|
await refreshMail()
|
||||||
renderChrome()
|
|
||||||
renderPages()
|
|
||||||
showMessagesPane('thread')
|
showMessagesPane('thread')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage(document.getElementById('thread-error'), err?.message || 'Could not load that conversation.')
|
setMessage(document.getElementById('thread-error'), err?.message || 'Could not load that conversation.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload inbox and dropdown conversations.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function refreshMail() {
|
||||||
|
if (!window.capsule || session?.preview) {
|
||||||
|
renderChrome()
|
||||||
|
renderPages()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [list, recent] = await Promise.all([
|
||||||
|
runApi(() => window.capsule.messages({ page: 1 })),
|
||||||
|
runApi(() => window.capsule.messagesRecent({ limit: 5 }))
|
||||||
|
])
|
||||||
|
if (list) {
|
||||||
|
messages = list
|
||||||
|
applyCanSend(list)
|
||||||
|
}
|
||||||
|
if (recent) {
|
||||||
|
recentMessages = recent
|
||||||
|
applyCanSend(recent)
|
||||||
|
}
|
||||||
|
renderChrome()
|
||||||
|
renderPages()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run site search and paint results.
|
* Run site search and paint results.
|
||||||
*
|
*
|
||||||
@ -1129,7 +1254,49 @@ document.getElementById('notification-list').addEventListener('click', async (ev
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
document.getElementById('message-list').addEventListener('click', (event) => {
|
document.getElementById('message-list').addEventListener('click', async (event) => {
|
||||||
|
const read = event.target.closest('[data-message-read]')
|
||||||
|
const unread = event.target.closest('[data-message-unread]')
|
||||||
|
const del = event.target.closest('[data-message-delete]')
|
||||||
|
if (read || unread || del) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
const id = read?.dataset.messageRead || unread?.dataset.messageUnread || del?.dataset.messageDelete
|
||||||
|
try {
|
||||||
|
if (session?.preview || !window.capsule) {
|
||||||
|
messages.items = (messages.items || []).filter((item) => {
|
||||||
|
if (String(item.id) !== String(id)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (del) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
item.unread = Boolean(unread)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
messages.unread = (messages.items || []).filter((item) => item.unread).length
|
||||||
|
if (recentMessages?.items) {
|
||||||
|
recentMessages.items = messages.items.slice(0, 5)
|
||||||
|
recentMessages.unread = messages.unread
|
||||||
|
}
|
||||||
|
renderChrome()
|
||||||
|
renderPages()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (read) {
|
||||||
|
await runApi(() => window.capsule.messageRead({ id }))
|
||||||
|
} else if (unread) {
|
||||||
|
await runApi(() => window.capsule.messageUnread({ id }))
|
||||||
|
} else {
|
||||||
|
await runApi(() => window.capsule.messageDelete({ id }))
|
||||||
|
}
|
||||||
|
await refreshMail()
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(document.getElementById('messages-empty'), err?.message || 'Could not update that conversation.')
|
||||||
|
document.getElementById('messages-empty').hidden = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
const row = event.target.closest('[data-message-id]')
|
const row = event.target.closest('[data-message-id]')
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return
|
return
|
||||||
@ -1283,7 +1450,7 @@ document.getElementById('settings-form').addEventListener('submit', async (event
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pendingAvatar = null
|
clearPendingAvatar()
|
||||||
document.getElementById('settings-avatar-file').value = ''
|
document.getElementById('settings-avatar-file').value = ''
|
||||||
applyProfile(data)
|
applyProfile(data)
|
||||||
renderChrome()
|
renderChrome()
|
||||||
@ -1297,20 +1464,20 @@ document.getElementById('settings-form').addEventListener('submit', async (event
|
|||||||
document.getElementById('settings-avatar-file').addEventListener('change', async (event) => {
|
document.getElementById('settings-avatar-file').addEventListener('change', async (event) => {
|
||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
pendingAvatar = null
|
clearPendingAvatar()
|
||||||
|
renderChrome()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const url = URL.createObjectURL(file)
|
if (pendingAvatarPreview) {
|
||||||
document.getElementById('settings-avatar').src = url
|
URL.revokeObjectURL(pendingAvatarPreview)
|
||||||
document.getElementById('profile-avatar').src = url
|
}
|
||||||
document.getElementById('header-avatar').src = url
|
pendingAvatarPreview = URL.createObjectURL(file)
|
||||||
document.getElementById('header-avatar').hidden = false
|
|
||||||
document.getElementById('header-avatar-fallback').hidden = true
|
|
||||||
pendingAvatar = {
|
pendingAvatar = {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
data: await file.arrayBuffer()
|
data: await file.arrayBuffer()
|
||||||
}
|
}
|
||||||
|
renderChrome()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.getElementById('settings-dark').addEventListener('change', (event) => {
|
document.getElementById('settings-dark').addEventListener('change', (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user