add messages
This commit is contained in:
@ -13,5 +13,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Password sign-in through `POST /api/login` and optional connect-with-token from Admin ? Tokens. MFA accounts continue in-app (`api/login/mfa/{loginCode}`) until a token is issued. `loginCode` stays in the main process; a pasted Admin token skips MFA.
|
||||
- Session stored in `userData`, encrypted with `safeStorage` when the OS allows it. The renderer never receives the token.
|
||||
- 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 and messages. 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.
|
||||
|
||||
@ -25,10 +25,10 @@ All HTTP runs in the **main process**. The renderer never sees the token and nev
|
||||
| MFA code / method | `POST /api/login/mfa/{loginCode}` | `auth_code` or `mfaMethodSelect`. `loginCode` stays in the main process. |
|
||||
| MFA reset | `POST /api/login/mfa/{loginCode}/reset` | Clears the chosen method so the picker shows again. |
|
||||
| Confirm identity | `GET /api/profile` | Bearer user token. Also used to hydrate username after login. `GET /api/users/find/{username}` remains available. |
|
||||
| Workspace | `GET /api/notifications`, `GET /api/messages` | First page after connect. Plugin-off responses show as unavailable. |
|
||||
| Workspace | `GET /api/notifications`, `GET /api/messages`, `GET /api/messages/recent` | First inbox page plus the header dropdown after connect. Plugin-off responses show as unavailable. |
|
||||
| Search | `GET /api/search` | Header search. `q`, `resource`, `page`. |
|
||||
| Profile save | `POST /api/profile/update` | Name, avatar, prefs. |
|
||||
| Mail / notices | `POST /api/messages/?`, `POST /api/notifications/?` | View, reply, create, read, delete. |
|
||||
| Mail / notices | `POST /api/messages/?`, `POST /api/notifications/?` | View, reply, create, read, unread, delete. |
|
||||
| Contact / bugs | `POST /api/contact`, `POST /api/bugreport` | Footer pages when those plugins are enabled. |
|
||||
| Existing token | Admin ? Tokens | Personal or app token. A user token hydrates the workspace; an app token can connect but cannot call the user API. |
|
||||
|
||||
|
||||
@ -14,10 +14,12 @@ import {
|
||||
listNotifications,
|
||||
readMessage,
|
||||
readNotification,
|
||||
recentMessages,
|
||||
replyMessage,
|
||||
searchSite,
|
||||
sendBugreport,
|
||||
sendContact,
|
||||
unreadMessage,
|
||||
unwrapApi,
|
||||
updateProfile,
|
||||
viewMessage
|
||||
@ -107,16 +109,18 @@ export function registerApiIpc() {
|
||||
session.apiReady = true
|
||||
writeSession(session)
|
||||
|
||||
const [notifications, messages] = await Promise.all([
|
||||
const [notifications, messages, recent] = await Promise.all([
|
||||
listNotifications(session.siteUrl, session.token, 1),
|
||||
listMessages(session.siteUrl, session.token, 1)
|
||||
listMessages(session.siteUrl, session.token, 1),
|
||||
recentMessages(session.siteUrl, session.token, 5)
|
||||
])
|
||||
|
||||
return {
|
||||
session: publicSession(session),
|
||||
profile,
|
||||
notifications: optionalList(notifications),
|
||||
messages: optionalList(messages)
|
||||
messages: optionalList(messages),
|
||||
recentMessages: optionalList(recent)
|
||||
}
|
||||
} catch (err) {
|
||||
dropDeadToken(session, err)
|
||||
@ -164,8 +168,22 @@ export function registerApiIpc() {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('capsule:messagesRecent', async (_event, payload) => {
|
||||
const session = requireSession()
|
||||
try {
|
||||
return optionalList(
|
||||
await recentMessages(session.siteUrl, session.token, payload?.limit || 5)
|
||||
)
|
||||
} catch (err) {
|
||||
dropDeadToken(session, err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('capsule:messageView', async (_event, payload) => {
|
||||
return runUserApi((session) => viewMessage(session.siteUrl, session.token, payload?.id))
|
||||
return runUserApi((session) =>
|
||||
viewMessage(session.siteUrl, session.token, payload?.id, payload?.markRead !== false)
|
||||
)
|
||||
})
|
||||
|
||||
ipcMain.handle('capsule:messageCreate', async (_event, payload) => {
|
||||
@ -184,6 +202,10 @@ export function registerApiIpc() {
|
||||
return runUserApi((session) => readMessage(session.siteUrl, session.token, payload?.id))
|
||||
})
|
||||
|
||||
ipcMain.handle('capsule:messageUnread', async (_event, payload) => {
|
||||
return runUserApi((session) => unreadMessage(session.siteUrl, session.token, payload?.id))
|
||||
})
|
||||
|
||||
ipcMain.handle('capsule:messageDelete', async (_event, payload) => {
|
||||
return runUserApi((session) => deleteMessage(session.siteUrl, session.token, payload?.id))
|
||||
})
|
||||
|
||||
@ -474,20 +474,40 @@ export async function listMessages(siteUrl, token, page = 1) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent conversations for the header dropdown. GET api/messages/recent.
|
||||
*
|
||||
* @param {string} siteUrl - canonical site base
|
||||
* @param {string} token - Bearer token
|
||||
* @param {number} [limit=5] - max rows
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
export async function recentMessages(siteUrl, token, limit = 5) {
|
||||
return ttpRequest({
|
||||
siteUrl,
|
||||
path: '/api/messages/recent',
|
||||
method: 'GET',
|
||||
token,
|
||||
query: { limit }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* One conversation. GET api/messages/view/{id}.
|
||||
*
|
||||
* @param {string} siteUrl - canonical site base
|
||||
* @param {string} token - Bearer token
|
||||
* @param {string|number} id - conversation id
|
||||
* @param {boolean} [markRead=true] - false sends markRead=0
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
export async function viewMessage(siteUrl, token, id) {
|
||||
export async function viewMessage(siteUrl, token, id, markRead = true) {
|
||||
return ttpRequest({
|
||||
siteUrl,
|
||||
path: `/api/messages/view/${encodeURIComponent(id)}`,
|
||||
method: 'GET',
|
||||
token
|
||||
token,
|
||||
query: markRead ? undefined : { markRead: 0 }
|
||||
})
|
||||
}
|
||||
|
||||
@ -546,6 +566,23 @@ export async function readMessage(siteUrl, token, id) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a conversation unread. POST api/messages/unread/{id}.
|
||||
*
|
||||
* @param {string} siteUrl - canonical site base
|
||||
* @param {string} token - Bearer token
|
||||
* @param {string|number} id - conversation id
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
export async function unreadMessage(siteUrl, token, id) {
|
||||
return ttpRequest({
|
||||
siteUrl,
|
||||
path: `/api/messages/unread/${encodeURIComponent(id)}`,
|
||||
method: 'POST',
|
||||
token
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a conversation. POST api/messages/delete/{id}.
|
||||
*
|
||||
|
||||
@ -154,10 +154,20 @@ const capsule = {
|
||||
return ipcRenderer.invoke('capsule:messages', payload)
|
||||
},
|
||||
|
||||
/**
|
||||
* Recent conversations for the header dropdown.
|
||||
*
|
||||
* @param {object} [payload] - limit
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
messagesRecent(payload) {
|
||||
return ipcRenderer.invoke('capsule:messagesRecent', payload)
|
||||
},
|
||||
|
||||
/**
|
||||
* One conversation thread.
|
||||
*
|
||||
* @param {object} payload - id
|
||||
* @param {object} payload - id, optional markRead
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
messageView(payload) {
|
||||
@ -194,6 +204,16 @@ const capsule = {
|
||||
return ipcRenderer.invoke('capsule:messageRead', payload)
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark a conversation unread.
|
||||
*
|
||||
* @param {object} payload - id
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
messageUnread(payload) {
|
||||
return ipcRenderer.invoke('capsule:messageUnread', payload)
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a conversation.
|
||||
*
|
||||
|
||||
@ -559,6 +559,7 @@
|
||||
<th>With</th>
|
||||
<th>Last message</th>
|
||||
<th>Updated</th>
|
||||
<th class="visually-hidden">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="message-list"></tbody>
|
||||
|
||||
@ -33,6 +33,7 @@ export const demoMessages = [
|
||||
id: 'm1',
|
||||
unread: true,
|
||||
otherUser: 'Alex',
|
||||
otherUserPretty: 'Alex',
|
||||
preview: 'Did the desktop shell pick up the new header?',
|
||||
lastMessageAt: 'Today'
|
||||
},
|
||||
@ -40,7 +41,8 @@ export const demoMessages = [
|
||||
id: 'm2',
|
||||
unread: false,
|
||||
otherUser: 'Sam',
|
||||
preview: 'Search should stay in the top middle <20> full width.',
|
||||
otherUserPretty: 'Sam',
|
||||
preview: 'Search should stay in the top middle <20> full width.',
|
||||
lastMessageAt: 'Monday'
|
||||
}
|
||||
]
|
||||
|
||||
@ -354,6 +354,13 @@ header .form-select:focus {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.capsule-drop-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.is-unread {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user