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.
|
- 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.
|
- 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.
|
- 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.
|
- 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 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. |
|
| 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. |
|
| 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`. |
|
| Search | `GET /api/search` | Header search. `q`, `resource`, `page`. |
|
||||||
| Profile save | `POST /api/profile/update` | Name, avatar, prefs. |
|
| 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. |
|
| 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. |
|
| 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,
|
listNotifications,
|
||||||
readMessage,
|
readMessage,
|
||||||
readNotification,
|
readNotification,
|
||||||
|
recentMessages,
|
||||||
replyMessage,
|
replyMessage,
|
||||||
searchSite,
|
searchSite,
|
||||||
sendBugreport,
|
sendBugreport,
|
||||||
sendContact,
|
sendContact,
|
||||||
|
unreadMessage,
|
||||||
unwrapApi,
|
unwrapApi,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
viewMessage
|
viewMessage
|
||||||
@ -107,16 +109,18 @@ export function registerApiIpc() {
|
|||||||
session.apiReady = true
|
session.apiReady = true
|
||||||
writeSession(session)
|
writeSession(session)
|
||||||
|
|
||||||
const [notifications, messages] = await Promise.all([
|
const [notifications, messages, recent] = await Promise.all([
|
||||||
listNotifications(session.siteUrl, session.token, 1),
|
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 {
|
return {
|
||||||
session: publicSession(session),
|
session: publicSession(session),
|
||||||
profile,
|
profile,
|
||||||
notifications: optionalList(notifications),
|
notifications: optionalList(notifications),
|
||||||
messages: optionalList(messages)
|
messages: optionalList(messages),
|
||||||
|
recentMessages: optionalList(recent)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dropDeadToken(session, 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) => {
|
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) => {
|
ipcMain.handle('capsule:messageCreate', async (_event, payload) => {
|
||||||
@ -184,6 +202,10 @@ export function registerApiIpc() {
|
|||||||
return runUserApi((session) => readMessage(session.siteUrl, session.token, payload?.id))
|
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) => {
|
ipcMain.handle('capsule:messageDelete', async (_event, payload) => {
|
||||||
return runUserApi((session) => deleteMessage(session.siteUrl, session.token, payload?.id))
|
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}.
|
* One conversation. GET api/messages/view/{id}.
|
||||||
*
|
*
|
||||||
* @param {string} siteUrl - canonical site base
|
* @param {string} siteUrl - canonical site base
|
||||||
* @param {string} token - Bearer token
|
* @param {string} token - Bearer token
|
||||||
* @param {string|number} id - conversation id
|
* @param {string|number} id - conversation id
|
||||||
|
* @param {boolean} [markRead=true] - false sends markRead=0
|
||||||
* @return {Promise<object>}
|
* @return {Promise<object>}
|
||||||
*/
|
*/
|
||||||
export async function viewMessage(siteUrl, token, id) {
|
export async function viewMessage(siteUrl, token, id, markRead = true) {
|
||||||
return ttpRequest({
|
return ttpRequest({
|
||||||
siteUrl,
|
siteUrl,
|
||||||
path: `/api/messages/view/${encodeURIComponent(id)}`,
|
path: `/api/messages/view/${encodeURIComponent(id)}`,
|
||||||
method: 'GET',
|
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}.
|
* Hide a conversation. POST api/messages/delete/{id}.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -154,10 +154,20 @@ const capsule = {
|
|||||||
return ipcRenderer.invoke('capsule:messages', payload)
|
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.
|
* One conversation thread.
|
||||||
*
|
*
|
||||||
* @param {object} payload - id
|
* @param {object} payload - id, optional markRead
|
||||||
* @return {Promise<object>}
|
* @return {Promise<object>}
|
||||||
*/
|
*/
|
||||||
messageView(payload) {
|
messageView(payload) {
|
||||||
@ -194,6 +204,16 @@ const capsule = {
|
|||||||
return ipcRenderer.invoke('capsule:messageRead', payload)
|
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.
|
* Hide a conversation.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -559,6 +559,7 @@
|
|||||||
<th>With</th>
|
<th>With</th>
|
||||||
<th>Last message</th>
|
<th>Last message</th>
|
||||||
<th>Updated</th>
|
<th>Updated</th>
|
||||||
|
<th class="visually-hidden">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="message-list"></tbody>
|
<tbody id="message-list"></tbody>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export const demoMessages = [
|
|||||||
id: 'm1',
|
id: 'm1',
|
||||||
unread: true,
|
unread: true,
|
||||||
otherUser: 'Alex',
|
otherUser: 'Alex',
|
||||||
|
otherUserPretty: 'Alex',
|
||||||
preview: 'Did the desktop shell pick up the new header?',
|
preview: 'Did the desktop shell pick up the new header?',
|
||||||
lastMessageAt: 'Today'
|
lastMessageAt: 'Today'
|
||||||
},
|
},
|
||||||
@ -40,7 +41,8 @@ export const demoMessages = [
|
|||||||
id: 'm2',
|
id: 'm2',
|
||||||
unread: false,
|
unread: false,
|
||||||
otherUser: 'Sam',
|
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'
|
lastMessageAt: 'Monday'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -354,6 +354,13 @@ header .form-select:focus {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.capsule-drop-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.is-unread {
|
.is-unread {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user