diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a731d..3bd7815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,13 @@ 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. POSTs send session CSRF (`X-CSRF-Token`); HTTP uses Electron `net.fetch` so the PHP session cookie persists. - 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, 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. -- 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. +- 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`) and post through `POST /api/contact` and `POST /api/bugreport`. Those footer items appear only when profile `features` (or `GET /api/contact` / `GET /api/bugreport`) say the plugin is available, accepting submissions, and allowed for this user. A hash to a disabled page shows the same “not accepting … right now” notice as the site. 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, and Terms of Service. Contact and Report a Bug join that band only when the site is accepting them. There is no subscribe box. + +### Changed + +- Footer upper band is Contact Us on the left, dark mode in the middle, and More Info on the right, in three equal columns. Contact and Report a Bug stay off the menu until the connected site says those plugins are accepting submissions. +- Profile page: space the white panel below the main nav, and cap the avatar at 200×200. ### Fixed diff --git a/README.md b/README.md index 583e851..b530b95 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ All HTTP runs in the **main process** (`net.fetch`, so the PHP session cookie st | Search | `GET /api/search` | Header search. `q`, `resource`, `page`. | | Profile save | `POST /api/profile/update` | Avatar and prefs. | | 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 | `GET` / `POST /api/contact`, `GET` / `POST /api/bugreport` | Footer pages when those plugins are enabled, accepting, and allowed. Hashing in while they are off shows a not-accepting notice. | | 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. | The token is stored under Electron `userData` (`session.json`). `safeStorage` encrypts it when the OS keychain is available. MFA `loginCode` is not stored. @@ -44,7 +44,7 @@ App-facing pairing notes live with the PHP app: `repos/ttp/docs/capsule.md`. | `src/preload/` | `window.capsule` bridge | | `src/renderer/` | Login, MFA, TTP-styled chrome, and live API views | -The logged-in header follows the public TTP shell (`text-bg-dark`, FA 6.7.1, Bootstrap 5.3). Search stays visible and centered. Account is a top-right dropdown like the site. Notifications and messages are the same bell / envelope menus. Profile edit covers User CP preferences except email, password, and phone ? those open `{site}/usercp/?`. Lists load from the site API after sign-in. The footer matches TTP copyright and social icons. Above that: dark-mode toggle, Privacy Policy, Terms of Service, Contact, and Report a Bug. No subscribe box. Privacy and terms open the connected site; contact and bug reports stay in-app. +The logged-in header follows the public TTP shell (`text-bg-dark`, FA 6.7.1, Bootstrap 5.3). Search stays visible and centered. Account is a top-right dropdown like the site. Notifications and messages are the same bell / envelope menus. Profile edit covers User CP preferences except email, password, and phone ? those open `{site}/usercp/?`. Lists load from the site API after sign-in. The footer matches TTP copyright and social icons. Above that: Contact Us on the left (Contact and Report a Bug only when the site is accepting them), dark-mode in the middle, More Info (Privacy Policy, Terms of Service) on the right. No subscribe box. Privacy and terms open the connected site; contact and bug reports stay in-app. ## Remote diff --git a/src/main/apiIpc.js b/src/main/apiIpc.js index c7b312f..40e7df1 100644 --- a/src/main/apiIpc.js +++ b/src/main/apiIpc.js @@ -8,6 +8,8 @@ import { createMessage, deleteMessage, deleteNotification, + getBugreportStatus, + getContactStatus, getProfile, isDeadTokenError, listMessages, @@ -221,22 +223,30 @@ export function registerApiIpc() { ) }) + ipcMain.handle('capsule:contactStatus', async () => { + return runUserApi((session) => getContactStatus(session.siteUrl, session.token)) + }) + ipcMain.handle('capsule:contact', async (_event, payload) => { return runUserApi((session) => sendContact(session.siteUrl, session.token, { name: payload?.name || '', entry: payload?.entry || '', - email: payload?.email || '' + contactEmail: payload?.contactEmail || payload?.email || '' }) ) }) + ipcMain.handle('capsule:bugreportStatus', async () => { + return runUserApi((session) => getBugreportStatus(session.siteUrl, session.token)) + }) + ipcMain.handle('capsule:bugreport', async (_event, payload) => { return runUserApi((session) => sendBugreport(session.siteUrl, session.token, { url: payload?.url || '', ourl: payload?.ourl || '', - repeat: payload?.repeat ? 'true' : 'false', + repeat: payload?.repeat, entry: payload?.entry || '' }) ) diff --git a/src/main/ttpClient.js b/src/main/ttpClient.js index 6a1911e..fb9725d 100644 --- a/src/main/ttpClient.js +++ b/src/main/ttpClient.js @@ -263,6 +263,14 @@ export function apiErrorMessage(code, errors) { 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.' + case 'Contact submissions are disabled.': + return 'The contact form is not accepting new messages right now.' + case 'Bug report submissions are disabled.': + return 'New bug reports are not being accepted right now.' + case 'Contact is not available': + return 'The contact form is not available.' + case 'Bug reports is not available': + return 'Bug reports are not available.' default: return code || 'The site returned an error.' } @@ -779,24 +787,54 @@ export async function searchSite(siteUrl, token, query) { }) } +/** + * Contact plugin status. GET api/contact. + * + * @param {string} siteUrl - canonical site base + * @param {string} token - Bearer token + * @return {Promise} + */ +export async function getContactStatus(siteUrl, token) { + return ttpRequest({ siteUrl, path: '/api/contact', method: 'GET', token }) +} + /** * Contact form. POST api/contact. * * @param {string} siteUrl - canonical site base * @param {string} token - Bearer token - * @param {object} fields - name, entry, optional email + * @param {object} fields - name, entry, optional contactEmail or email * @return {Promise} */ export async function sendContact(siteUrl, token, fields) { + const form = { + name: fields?.name || '', + entry: fields?.entry || '' + } + const email = fields?.contactEmail || fields?.email || '' + if (email) { + form.contactEmail = email + } return ttpRequest({ siteUrl, path: '/api/contact', method: 'POST', token, - form: fields + form }) } +/** + * Bug report plugin status. GET api/bugreport. + * + * @param {string} siteUrl - canonical site base + * @param {string} token - Bearer token + * @return {Promise} + */ +export async function getBugreportStatus(siteUrl, token) { + return ttpRequest({ siteUrl, path: '/api/bugreport', method: 'GET', token }) +} + /** * Bug report. POST api/bugreport. * @@ -806,11 +844,18 @@ export async function sendContact(siteUrl, token, fields) { * @return {Promise} */ export async function sendBugreport(siteUrl, token, fields) { + const url = fields?.url || '' + const ourl = fields?.ourl || url return ttpRequest({ siteUrl, path: '/api/bugreport', method: 'POST', token, - form: fields + form: { + url, + ourl, + repeat: fields?.repeat === true || fields?.repeat === 'true' ? 'true' : 'false', + entry: fields?.entry || '' + } }) } diff --git a/src/preload/index.js b/src/preload/index.js index 4fb5cad..0627901 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -234,16 +234,34 @@ const capsule = { return ipcRenderer.invoke('capsule:search', payload) }, + /** + * Contact plugin status. GET api/contact. + * + * @return {Promise} + */ + contactStatus() { + return ipcRenderer.invoke('capsule:contactStatus') + }, + /** * Contact form submit. * - * @param {object} payload - name, entry, email + * @param {object} payload - name, entry, contactEmail or email * @return {Promise} */ contact(payload) { return ipcRenderer.invoke('capsule:contact', payload) }, + /** + * Bug report plugin status. GET api/bugreport. + * + * @return {Promise} + */ + bugreportStatus() { + return ipcRenderer.invoke('capsule:bugreportStatus') + }, + /** * Bug report submit. * diff --git a/src/renderer/index.html b/src/renderer/index.html index ed0a821..b61dca4 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -358,8 +358,8 @@

Contact Us

- -
+ +