bug reports and contacts
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 || ''
|
||||
})
|
||||
)
|
||||
|
||||
@ -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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
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 || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -234,16 +234,34 @@ const capsule = {
|
||||
return ipcRenderer.invoke('capsule:search', payload)
|
||||
},
|
||||
|
||||
/**
|
||||
* Contact plugin status. GET api/contact.
|
||||
*
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
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<object>}
|
||||
*/
|
||||
contact(payload) {
|
||||
return ipcRenderer.invoke('capsule:contact', payload)
|
||||
},
|
||||
|
||||
/**
|
||||
* Bug report plugin status. GET api/bugreport.
|
||||
*
|
||||
* @return {Promise<object>}
|
||||
*/
|
||||
bugreportStatus() {
|
||||
return ipcRenderer.invoke('capsule:bugreportStatus')
|
||||
},
|
||||
|
||||
/**
|
||||
* Bug report submit.
|
||||
*
|
||||
|
||||
@ -358,8 +358,8 @@
|
||||
<div class="m-2 m-lg-4">
|
||||
<div class="context-main-bg container py-2 my-2 py-lg-4 my-lg-4">
|
||||
<h1 class="h2 text-center mb-4">Contact Us</h1>
|
||||
<p id="contact-unavailable" class="text-muted text-center" hidden></p>
|
||||
<div id="contact-body">
|
||||
<p id="contact-unavailable" class="alert alert-warning text-center" hidden></p>
|
||||
<div id="contact-body" hidden>
|
||||
<div class="col-12 col-lg-6 offset-lg-3">
|
||||
<p class="text-center text-lg-start">
|
||||
Here at <strong id="contact-sitename">this site</strong>, we highly value your
|
||||
@ -381,6 +381,7 @@
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
maxlength="20"
|
||||
autocomplete="name"
|
||||
/>
|
||||
</div>
|
||||
@ -393,7 +394,7 @@
|
||||
<input
|
||||
id="contact-email"
|
||||
class="form-control"
|
||||
name="email"
|
||||
name="contactEmail"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
/>
|
||||
@ -428,8 +429,8 @@
|
||||
<div class="col-12 col-sm-10 col-lg-8 mx-auto p-4 rounded shadow-sm context-main-bg">
|
||||
<h1 class="h2 text-center mb-4">Report a Bug</h1>
|
||||
<hr />
|
||||
<p id="bug-unavailable" class="text-muted text-center" hidden></p>
|
||||
<div id="bug-body">
|
||||
<p id="bug-unavailable" class="alert alert-warning text-center" hidden></p>
|
||||
<div id="bug-body" hidden>
|
||||
<p class="text-center text-sm-start">
|
||||
Thank you for visiting our bug reporting page. We value our users' input highly and
|
||||
in an effort to better serve your needs, please fill out the form below to help us
|
||||
@ -599,46 +600,49 @@
|
||||
</section>
|
||||
|
||||
<section id="view-profile" class="view" hidden>
|
||||
<div class="container p-4 context-main-bg mb-4 text-center">
|
||||
<h1 class="h3 mb-4">Profile</h1>
|
||||
<hr />
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header text-center bg-dark text-white">
|
||||
<h2 class="h3 card-title mb-0" id="profile-name"></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4 text-center">
|
||||
<img
|
||||
id="profile-avatar"
|
||||
src=""
|
||||
alt="User Pic"
|
||||
class="rounded-circle img-fluid"
|
||||
style="max-width: 150px"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<table class="table table-borderless text-start">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Registered:</th>
|
||||
<td id="profile-registered"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Last Seen:</th>
|
||||
<td id="profile-last-login"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Gender:</th>
|
||||
<td id="profile-gender"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="#/settings" class="btn btn-primary btn-sm" data-route="settings">
|
||||
<i class="fa fa-fw fa-pencil"></i> Edit profile
|
||||
</a>
|
||||
<div class="m-2 m-lg-4">
|
||||
<div class="context-main-bg container py-2 my-2 py-lg-4 my-lg-4 text-center">
|
||||
<h1 class="h3 mb-4">Profile</h1>
|
||||
<hr />
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header text-center bg-dark text-white">
|
||||
<h2 class="h3 card-title mb-0" id="profile-name"></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4 text-center">
|
||||
<img
|
||||
id="profile-avatar"
|
||||
src=""
|
||||
alt="User Pic"
|
||||
class="rounded-circle capsule-profile-avatar"
|
||||
width="200"
|
||||
height="200"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<table class="table table-borderless text-start">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Registered:</th>
|
||||
<td id="profile-registered"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Last Seen:</th>
|
||||
<td id="profile-last-login"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Gender:</th>
|
||||
<td id="profile-gender"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="#/settings" class="btn btn-primary btn-sm" data-route="settings">
|
||||
<i class="fa fa-fw fa-pencil"></i> Edit profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -746,19 +750,26 @@
|
||||
</div>
|
||||
|
||||
<div class="collapse d-md-block my-4" id="footerMenu">
|
||||
<div class="row">
|
||||
<div id="footer-contact-col" class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3 text-center">
|
||||
<div class="capsule-footer-cols">
|
||||
<div id="footer-contact-col" class="capsule-footer-contact" hidden>
|
||||
<h2 class="h5">Contact Us</h2>
|
||||
<ul class="nav flex-column">
|
||||
<li id="footer-link-contact" class="nav-item mb-2">
|
||||
<li id="footer-link-contact" class="nav-item mb-2" hidden>
|
||||
<a href="#/contact" class="nav-link p-0 text-muted">Contact</a>
|
||||
</li>
|
||||
<li id="footer-link-bug" class="nav-item mb-2">
|
||||
<li id="footer-link-bug" class="nav-item mb-2" hidden>
|
||||
<a href="#/bugreport" class="nav-link p-0 text-muted">Report a Bug</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3 text-center">
|
||||
<div class="capsule-footer-theme">
|
||||
<h2 class="h5">Dark Mode</h2>
|
||||
<div class="material-switch px-4 mt-2">
|
||||
<input name="dark-mode-toggle" type="checkbox" id="dark-mode-toggle" class="form-check-input" />
|
||||
<label for="dark-mode-toggle" class="label-default"><span class="visually-hidden">Dark mode</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="capsule-footer-info">
|
||||
<h2 class="h5">More Info</h2>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item mb-2">
|
||||
@ -783,13 +794,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3 text-center">
|
||||
<h2 class="h5">Dark Mode</h2>
|
||||
<div class="material-switch px-4 mt-2">
|
||||
<input name="dark-mode-toggle" type="checkbox" id="dark-mode-toggle" class="form-check-input" />
|
||||
<label for="dark-mode-toggle" class="label-default"><span class="visually-hidden">Dark mode</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -50,6 +50,10 @@ const previewButton = document.getElementById('preview-shell')
|
||||
let session = null
|
||||
let profile = { ...demoProfile }
|
||||
let plugins = []
|
||||
let features = {
|
||||
contact: { available: false, accepting: false, canSubmit: false },
|
||||
bugreport: { available: false, accepting: false, canSubmit: false }
|
||||
}
|
||||
let notifications = { items: [], unread: 0 }
|
||||
let messages = { items: [], unread: 0 }
|
||||
let recentMessages = null
|
||||
@ -122,6 +126,130 @@ function hasPlugin(name) {
|
||||
return plugins.includes(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize Capsule feature flags from the API.
|
||||
*
|
||||
* @param {object} [raw] - available, accepting, canSubmit
|
||||
* @return {{available: boolean, accepting: boolean, canSubmit: boolean}}
|
||||
*/
|
||||
function normalizeFeature(raw) {
|
||||
return {
|
||||
available: Boolean(raw?.available),
|
||||
accepting: Boolean(raw?.accepting),
|
||||
canSubmit: Boolean(raw?.canSubmit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status for contact or bugreport from profile, GET, or the plugin list.
|
||||
*
|
||||
* @param {string} name - contact or bugreport
|
||||
* @return {{available: boolean, accepting: boolean, canSubmit: boolean}}
|
||||
*/
|
||||
function featureState(name) {
|
||||
return features[name] || { available: false, accepting: false, canSubmit: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* True when that footer item should appear (plugin on, accepting, permitted).
|
||||
*
|
||||
* @param {string} name - contact or bugreport
|
||||
* @return {boolean}
|
||||
*/
|
||||
function featureInMenu(name) {
|
||||
if (session?.preview) {
|
||||
return true
|
||||
}
|
||||
const state = featureState(name)
|
||||
return state.available && state.accepting && state.canSubmit
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy for a contact or bugreport page that is not taking submissions.
|
||||
*
|
||||
* @param {string} name - contact or bugreport
|
||||
* @return {string}
|
||||
*/
|
||||
function featureGateMessage(name) {
|
||||
const state = featureState(name)
|
||||
if (name === 'contact') {
|
||||
if (!state.available) {
|
||||
return 'The contact form is not available.'
|
||||
}
|
||||
if (!state.accepting) {
|
||||
return 'The contact form is not accepting new messages right now.'
|
||||
}
|
||||
if (!state.canSubmit) {
|
||||
return 'You do not have permission to use the contact form.'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (!state.available) {
|
||||
return 'Bug reports are not available.'
|
||||
}
|
||||
if (!state.accepting) {
|
||||
return 'New bug reports are not being accepted right now.'
|
||||
}
|
||||
if (!state.canSubmit) {
|
||||
return 'You do not have permission to report bugs.'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Store contact/bugreport flags from profile JSON or a GET status body.
|
||||
*
|
||||
* @param {object} [body] - GET/POST api/profile or GET api/contact|bugreport
|
||||
* @return {void}
|
||||
*/
|
||||
function applyFeatures(body) {
|
||||
if (body?.features && typeof body.features === 'object') {
|
||||
features = {
|
||||
contact: normalizeFeature(body.features.contact),
|
||||
bugreport: normalizeFeature(body.features.bugreport)
|
||||
}
|
||||
return
|
||||
}
|
||||
features = {
|
||||
contact: featureFromPlugin('contact'),
|
||||
bugreport: featureFromPlugin('bugreport')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge one GET status payload into local feature flags.
|
||||
*
|
||||
* @param {string} name - contact or bugreport
|
||||
* @param {object} [body] - GET api/contact or GET api/bugreport
|
||||
* @return {void}
|
||||
*/
|
||||
function applyFeatureStatus(name, body) {
|
||||
if (body && typeof body === 'object' && !body.error) {
|
||||
if ('available' in body || 'accepting' in body || 'canSubmit' in body) {
|
||||
features = {
|
||||
...features,
|
||||
[name]: normalizeFeature(body)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
features = {
|
||||
...features,
|
||||
[name]: featureFromPlugin(name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature flags from the enabled-plugin list when the site has no features blob.
|
||||
*
|
||||
* @param {string} name - plugin folder
|
||||
* @return {{available: boolean, accepting: boolean, canSubmit: boolean}}
|
||||
*/
|
||||
function featureFromPlugin(name) {
|
||||
const on = hasPlugin(name)
|
||||
return { available: on, accepting: on, canSubmit: on }
|
||||
}
|
||||
|
||||
/**
|
||||
* Site path that opens in the system browser.
|
||||
*
|
||||
@ -267,6 +395,7 @@ function timezoneList() {
|
||||
function applyProfile(body) {
|
||||
const user = body?.user || {}
|
||||
plugins = Array.isArray(body?.plugins) ? body.plugins : []
|
||||
applyFeatures(body)
|
||||
profile = {
|
||||
id: user.id,
|
||||
username: user.username || session?.username || '',
|
||||
@ -462,11 +591,11 @@ function renderFooter() {
|
||||
}
|
||||
})
|
||||
|
||||
const showContact = Boolean(session?.connected) && (session.preview || hasPlugin('contact'))
|
||||
const showBug = Boolean(session?.connected) && (session.preview || hasPlugin('bugreport'))
|
||||
document.getElementById('footer-link-contact').hidden = !showContact
|
||||
document.getElementById('footer-link-bug').hidden = !showBug
|
||||
document.getElementById('footer-contact-col').hidden = !showContact && !showBug
|
||||
const contactOn = featureInMenu('contact')
|
||||
const bugOn = featureInMenu('bugreport')
|
||||
document.getElementById('footer-link-contact').hidden = !contactOn
|
||||
document.getElementById('footer-link-bug').hidden = !bugOn
|
||||
document.getElementById('footer-contact-col').hidden = !contactOn && !bugOn
|
||||
}
|
||||
|
||||
/**
|
||||
@ -564,19 +693,18 @@ function renderPages() {
|
||||
? `Enabled plugins: ${plugins.join(', ')}`
|
||||
: 'Plugin list will show after profile loads.'
|
||||
|
||||
const contactOk = session.preview || hasPlugin('contact')
|
||||
const bugOk = session.preview || hasPlugin('bugreport')
|
||||
const contactOk = featureInMenu('contact')
|
||||
const bugOk = featureInMenu('bugreport')
|
||||
document.getElementById('contact-body').hidden = !contactOk
|
||||
document.getElementById('bug-body').hidden = !bugOk
|
||||
const contactUnavailable = document.getElementById('contact-unavailable')
|
||||
const bugUnavailable = document.getElementById('bug-unavailable')
|
||||
contactUnavailable.hidden = contactOk
|
||||
bugUnavailable.hidden = bugOk
|
||||
contactUnavailable.textContent = contactOk ? '' : 'Contact is not available on this site.'
|
||||
bugUnavailable.textContent = bugOk ? '' : 'Bug reports are not available on this site.'
|
||||
if (contactOk && !document.getElementById('contact-name').value) {
|
||||
document.getElementById('contact-name').value = profile.username || session.username || ''
|
||||
document.getElementById('contact-email').value = profile.email || ''
|
||||
contactUnavailable.textContent = contactOk ? '' : featureGateMessage('contact')
|
||||
bugUnavailable.textContent = bugOk ? '' : featureGateMessage('bugreport')
|
||||
if (contactOk && !document.getElementById('contact-email').value && profile.email) {
|
||||
document.getElementById('contact-email').value = profile.email
|
||||
}
|
||||
if (bugOk && session.siteUrl && !document.getElementById('bug-url').value) {
|
||||
document.getElementById('bug-url').value = session.siteUrl
|
||||
@ -888,10 +1016,16 @@ function route() {
|
||||
}
|
||||
if (path === 'contact') {
|
||||
showView('contact')
|
||||
if (!session.preview) {
|
||||
loadFeatureStatus('contact')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (path === 'bugreport') {
|
||||
showView('bugreport')
|
||||
if (!session.preview) {
|
||||
loadFeatureStatus('bugreport')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (path === 'search') {
|
||||
@ -928,6 +1062,10 @@ function applySession(next) {
|
||||
window.location.hash = ''
|
||||
applyTheme(false)
|
||||
plugins = []
|
||||
features = {
|
||||
contact: { available: false, accepting: false, canSubmit: false },
|
||||
bugreport: { available: false, accepting: false, canSubmit: false }
|
||||
}
|
||||
notifications = { items: [], unread: 0 }
|
||||
messages = { items: [], unread: 0 }
|
||||
recentMessages = null
|
||||
@ -951,7 +1089,11 @@ function applySession(next) {
|
||||
recentMessages = { items: demoMessages, unread: messages.unread }
|
||||
canSendMessages = true
|
||||
profile = { ...demoProfile }
|
||||
plugins = []
|
||||
plugins = ['contact', 'bugreport']
|
||||
features = {
|
||||
contact: { available: true, accepting: true, canSubmit: true },
|
||||
bugreport: { available: true, accepting: true, canSubmit: true }
|
||||
}
|
||||
} else if (session?.connected && window.capsule) {
|
||||
loadWorkspace()
|
||||
}
|
||||
@ -992,6 +1134,33 @@ async function loadWorkspace() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recheck whether contact or bug reports are accepting submissions.
|
||||
*
|
||||
* @param {string} name - contact or bugreport
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function loadFeatureStatus(name) {
|
||||
if (!window.capsule || session?.preview) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await runApi(() =>
|
||||
name === 'contact' ? window.capsule.contactStatus() : window.capsule.bugreportStatus()
|
||||
)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
applyFeatureStatus(name, data)
|
||||
renderChrome()
|
||||
renderPages()
|
||||
} catch {
|
||||
applyFeatureStatus(name, null)
|
||||
renderChrome()
|
||||
renderPages()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load one conversation.
|
||||
*
|
||||
@ -1362,6 +1531,11 @@ document.getElementById('contact-form').addEventListener('submit', async (event)
|
||||
const button = event.target.querySelector('button[type="submit"]')
|
||||
button.disabled = true
|
||||
setMessage(status, '')
|
||||
if (!featureInMenu('contact')) {
|
||||
setMessage(status, featureGateMessage('contact'), 'text-danger')
|
||||
button.disabled = false
|
||||
return
|
||||
}
|
||||
if (session?.preview || !window.capsule) {
|
||||
setMessage(status, 'Saved in preview only.', 'text-muted')
|
||||
button.disabled = false
|
||||
@ -1371,14 +1545,18 @@ document.getElementById('contact-form').addEventListener('submit', async (event)
|
||||
await runApi(() =>
|
||||
window.capsule.contact({
|
||||
name: document.getElementById('contact-name').value.trim(),
|
||||
email: document.getElementById('contact-email').value.trim(),
|
||||
contactEmail: document.getElementById('contact-email').value.trim(),
|
||||
entry: document.getElementById('contact-entry').value
|
||||
})
|
||||
)
|
||||
document.getElementById('contact-entry').value = ''
|
||||
setMessage(status, 'Sent.', 'text-success')
|
||||
} catch (err) {
|
||||
setMessage(status, err?.message || 'Could not send that message.', 'text-danger')
|
||||
const msg = err?.message || 'Could not send that message.'
|
||||
if (/disabled|not available|permission/i.test(msg)) {
|
||||
loadFeatureStatus('contact')
|
||||
}
|
||||
setMessage(status, msg, 'text-danger')
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
@ -1390,16 +1568,23 @@ document.getElementById('bugreport-form').addEventListener('submit', async (even
|
||||
const button = event.target.querySelector('button[type="submit"]')
|
||||
button.disabled = true
|
||||
setMessage(status, '')
|
||||
if (!featureInMenu('bugreport')) {
|
||||
setMessage(status, featureGateMessage('bugreport'), 'text-danger')
|
||||
button.disabled = false
|
||||
return
|
||||
}
|
||||
if (session?.preview || !window.capsule) {
|
||||
setMessage(status, 'Saved in preview only.', 'text-muted')
|
||||
button.disabled = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const url = document.getElementById('bug-url').value.trim()
|
||||
const ourl = document.getElementById('bug-ourl').value.trim() || url
|
||||
await runApi(() =>
|
||||
window.capsule.bugreport({
|
||||
url: document.getElementById('bug-url').value.trim(),
|
||||
ourl: document.getElementById('bug-ourl').value.trim(),
|
||||
url,
|
||||
ourl,
|
||||
repeat: document.getElementById('bug-repeat-yes').checked,
|
||||
entry: document.getElementById('bug-entry').value
|
||||
})
|
||||
@ -1407,7 +1592,11 @@ document.getElementById('bugreport-form').addEventListener('submit', async (even
|
||||
document.getElementById('bug-entry').value = ''
|
||||
setMessage(status, 'Sent.', 'text-success')
|
||||
} catch (err) {
|
||||
setMessage(status, err?.message || 'Could not send that report.', 'text-danger')
|
||||
const msg = err?.message || 'Could not send that report.'
|
||||
if (/disabled|not available|permission/i.test(msg)) {
|
||||
loadFeatureStatus('bugreport')
|
||||
}
|
||||
setMessage(status, msg, 'text-danger')
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
|
||||
@ -107,6 +107,63 @@ footer a.context-main {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer upper band: Contact Us, dark mode, More Info.
|
||||
*/
|
||||
.capsule-footer-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
'contact'
|
||||
'theme'
|
||||
'info';
|
||||
gap: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.capsule-footer-contact {
|
||||
grid-area: contact;
|
||||
}
|
||||
|
||||
.capsule-footer-cols:not(:has(#footer-contact-col:not([hidden]))) {
|
||||
grid-template-areas:
|
||||
'theme'
|
||||
'info';
|
||||
}
|
||||
|
||||
.capsule-footer-theme {
|
||||
grid-area: theme;
|
||||
}
|
||||
|
||||
.capsule-footer-info {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.capsule-footer-cols {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-areas: 'contact theme info';
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.capsule-footer-contact {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.capsule-footer-theme {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.capsule-footer-info {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.capsule-footer-cols:not(:has(#footer-contact-col:not([hidden]))) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas: 'theme info';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark-mode footer switch.
|
||||
*/
|
||||
@ -361,6 +418,17 @@ header .form-select:focus {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile page avatar.
|
||||
*/
|
||||
.capsule-profile-avatar {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.is-unread {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user