Compare commits
4
Commits
0c6003fb87
...
1b2142e6c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b2142e6c4 | ||
|
|
4236621c7a | ||
|
|
45afc9ba7e | ||
|
|
fd87a27251 |
+12
-12
@@ -11,58 +11,58 @@
|
||||
"theme_color": "#980000",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./res/android/android-chrome-36x36.png",
|
||||
"src": "./public/res/android/android-chrome-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-48x48.png",
|
||||
"src": "./public/res/android/android-chrome-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-72x72.png",
|
||||
"src": "./public/res/android/android-chrome-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-96x96.png",
|
||||
"src": "./public/res/android/android-chrome-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-144x144.png",
|
||||
"src": "./public/res/android/android-chrome-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-192x192.png",
|
||||
"src": "./public/res/android/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-256x256.png",
|
||||
"src": "./public/res/android/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-384x384.png",
|
||||
"src": "./public/res/android/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-512x512.png",
|
||||
"src": "./public/res/android/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/maskable-192x192.png",
|
||||
"src": "./public/res/android/maskable-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/maskable-512x512.png",
|
||||
"src": "./public/res/android/maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
@@ -77,7 +77,7 @@
|
||||
"url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "res/android/android-chrome-96x96.png",
|
||||
"src": "public/res/android/android-chrome-96x96.png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -29,7 +29,11 @@ function fetchDecoration(
|
||||
const waiters: Array<(val: string | null) => void> = [];
|
||||
pending.set(userId, waiters);
|
||||
|
||||
return authedRequest(Method.Get, `/profile/${encodeURIComponent(userId)}/${PROFILE_FIELD}`)
|
||||
// Fetch the WHOLE profile, not the single `/{field}` sub-resource: an unset
|
||||
// field returns 404, which the browser logs as a failed request — a console
|
||||
// 404 for every user without a decoration. The full profile returns 200 with
|
||||
// all fields (incl. custom MSC4133 ones); read the decoration out of it.
|
||||
return authedRequest(Method.Get, `/profile/${encodeURIComponent(userId)}`)
|
||||
.then((res) => {
|
||||
const val = (res[PROFILE_FIELD] as string | undefined) ?? null;
|
||||
cache.set(userId, val);
|
||||
@@ -37,12 +41,9 @@ function fetchDecoration(
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const status = err instanceof MatrixError ? err.httpStatus : undefined;
|
||||
// Definitive rejections — the field is unset (404) or the server won't
|
||||
// serve it (400/403). This is the common case for FEDERATED users whose
|
||||
// homeserver doesn't support extended profiles / rejects the field. Cache
|
||||
// "no decoration" so we never refetch: otherwise every avatar mount
|
||||
// re-requests and floods our homeserver with failing federated profile
|
||||
// lookups (the 403/502 console storm + real HS load).
|
||||
// Definitive rejections (404 unknown user / 403 can't view / 400) — cache
|
||||
// "no decoration" so we never refetch a profile we can't read (otherwise
|
||||
// every avatar mount re-floods our HS with failing federated lookups).
|
||||
if (status === 404 || status === 403 || status === 400) {
|
||||
cache.set(userId, null);
|
||||
} else {
|
||||
|
||||
@@ -216,15 +216,18 @@ test('Instagram: p / reel / tv → embed path', () => {
|
||||
assert.equal(getInstagramEmbed('https://www.instagram.com/someuser/'), null);
|
||||
});
|
||||
|
||||
test('Reddit post embed → redditmedia', () => {
|
||||
test('Reddit post embed → embed.reddit.com', () => {
|
||||
assert.equal(
|
||||
getRedditPostEmbed('https://www.reddit.com/r/aww/comments/abc123/cute_cat/'),
|
||||
'https://embed.reddit.com/r/aww/comments/abc123/?ref_source=embed&ref=share&embed=true&theme=dark',
|
||||
);
|
||||
assert.equal(getRedditPostEmbed('https://old.reddit.com/r/aww/comments/xyz/'), 'https://embed.reddit.com/r/aww/comments/xyz/?ref_source=embed&ref=share&embed=true&theme=dark');
|
||||
assert.equal(getRedditPostEmbed('https://www.reddit.com/r/aww/'), null); // subreddit, not a post
|
||||
// redd.it short link → comments-only embed (no subreddit in the redirect)
|
||||
assert.equal(getRedditPostEmbed('https://redd.it/1abc23'), 'https://embed.reddit.com/comments/1abc23/?ref_source=embed&ref=share&embed=true&theme=dark');
|
||||
// redd.it short link / i.redd.it media host can't build the /r/<sub>/comments/<id>
|
||||
// path embed.reddit.com requires (a bare /comments/<id> 404s) → null so the caller's
|
||||
// og:url fallback resolves the canonical URL first.
|
||||
assert.equal(getRedditPostEmbed('https://redd.it/1abc23'), null);
|
||||
assert.equal(getRedditPostEmbed('https://i.redd.it/abcd1234.jpg'), null);
|
||||
});
|
||||
|
||||
test('parseMediaEmbed: Instagram/Reddit → rich, Tidal → audio', () => {
|
||||
|
||||
@@ -344,8 +344,9 @@ export function getInstagramEmbed(url: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reddit (post embed via redditmedia — bypasses the homeserver's blocked
|
||||
// preview fetch, which Reddit serves a bot-check "please wait" page to) ---
|
||||
// --- Reddit (post embed via embed.reddit.com — reddit's own oEmbed widget host;
|
||||
// www.redditmedia.com now 301s here. Bypasses the homeserver's blocked preview
|
||||
// fetch, which Reddit serves a bot-check "please wait" page to) ---
|
||||
|
||||
const REDDIT_EMBED_QS = '?ref_source=embed&ref=share&embed=true&theme=dark';
|
||||
|
||||
@@ -353,15 +354,14 @@ export function getRedditPostEmbed(url: string): string | null {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const h = u.hostname.replace(/^(www|old|new|np|i)\./, '');
|
||||
// redd.it/<id> short link → reddit.com/comments/<id> (no subreddit needed).
|
||||
if (h === 'redd.it') {
|
||||
const id = u.pathname.replace(/^\/+|\/+$/g, '').split('/')[0];
|
||||
return id ? `https://embed.reddit.com/comments/${id}/${REDDIT_EMBED_QS}` : null;
|
||||
}
|
||||
// embed.reddit.com ONLY renders the /r/<sub>/comments/<id> path (verified against
|
||||
// reddit's own embed widgets.js). A bare /comments/<id> — all we could build from a
|
||||
// redd.it short link or an i./v.redd.it media host — serves a "not found" page, so
|
||||
// return null for those and let the caller's og:url fallback resolve the canonical
|
||||
// /r/<sub>/comments/<id> URL (from the redirect) before re-parsing into a real embed.
|
||||
if (h !== 'reddit.com') return null;
|
||||
const m = u.pathname.match(/^\/r\/([A-Za-z0-9_]+)\/comments\/([A-Za-z0-9]+)/);
|
||||
if (!m) return null;
|
||||
// embed.reddit.com is the current host (www.redditmedia.com now 301s here).
|
||||
return `https://embed.reddit.com/r/${m[1]}/comments/${m[2]}/${REDDIT_EMBED_QS}`;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -12,7 +12,33 @@ import { deleteSearchCacheDatabase } from '../app/utils/searchCache';
|
||||
// This happens after a downgrade (e.g. matrix-js-sdk was briefly upgraded and then reverted).
|
||||
export const IDB_VERSION_CONFLICT = 'IDB_VERSION_CONFLICT';
|
||||
|
||||
/**
|
||||
* KE-1 mitigation. Ask the browser to make this origin's storage persistent so the
|
||||
* IndexedDB **crypto store** isn't evicted from under a surviving `localStorage`
|
||||
* session. When that happens the device "resurrects" with a blank key store and the
|
||||
* client re-uploads a one-time key at an id Synapse already holds → a permanent
|
||||
* `400 M_UNKNOWN: … already exists` upload-conflict storm (and, downstream,
|
||||
* undecryptable to-device/media keys). `persist()` grants based on engagement / PWA
|
||||
* install and shows no prompt; denial/absence is non-fatal.
|
||||
*/
|
||||
export const requestPersistentStorage = async (): Promise<boolean> => {
|
||||
try {
|
||||
if (!navigator.storage?.persist) return false;
|
||||
if (await navigator.storage.persisted()) return true;
|
||||
const granted = await navigator.storage.persist();
|
||||
if (!granted) {
|
||||
console.warn('Persistent storage not granted — the crypto store remains evictable (KE-1).');
|
||||
}
|
||||
return granted;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||
// Protect the crypto store from eviction before we create and write to it.
|
||||
await requestPersistentStorage();
|
||||
|
||||
const indexedDBStore = new IndexedDBStore({
|
||||
indexedDB: globalThis.indexedDB,
|
||||
localStorage: globalThis.localStorage,
|
||||
|
||||
+11
-2
@@ -38,9 +38,18 @@ const copyFiles = {
|
||||
rename: { stripBase: true },
|
||||
},
|
||||
{
|
||||
src: 'public/res/android',
|
||||
// All of public/res/ (android + apple icons, logos, svg) → dist/public/res/,
|
||||
// matching the /public/res/... paths used by index.html and manifest.json.
|
||||
// (The old android-only target stripped `res/` and landed at /public/android/,
|
||||
// so every icon 404'd — apple/ + favicon weren't copied at all.)
|
||||
src: 'public/res',
|
||||
dest: 'public/',
|
||||
rename: { stripBase: 2 },
|
||||
rename: { stripBase: 1 },
|
||||
},
|
||||
{
|
||||
src: 'public/favicon.ico',
|
||||
dest: 'public/',
|
||||
rename: { stripBase: 1 },
|
||||
},
|
||||
{
|
||||
src: 'public/locales',
|
||||
|
||||
Reference in New Issue
Block a user