Compare commits

...

17 Commits

Author SHA1 Message Date
Ajay Bura a95962f991 allow nested list in markdown 2026-05-13 07:28:37 +05:30
Ajay Bura 0295f785e6 allow codeblock plaintext inside codeblock by extending fence count 2026-05-09 19:22:57 +05:30
Ajay Bura 1fb7e0ebf1 remove unused imports 2026-05-09 18:41:34 +05:30
Ajay Bura 7c349978ac fix crash when editing message with empty trailing heading 2026-05-09 18:41:15 +05:30
Shea 341fedd932 fix: edit lists crashing and list rendering issue in Firefox (#2920)
* one liner fix editor

* fix firefox rendering lists

* moved fixes

* moved fixes per ajbura
2026-05-03 22:15:08 +10:00
dependabot[bot] d186d31399 chore(deps): bump actions/setup-node from 6.3.0 to 6.4.0 (#2906)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 10:12:30 +10:00
James 098684973e fix: do not attempt to join call on doubleclick if missing permissions (#2798)
* fix: do not attempt to join call on doubleclick if missing permissions

* update comment

* export getPowersLevelFromMatrixEvent for usage elsewhere

* only read vc permissions when actually needed instead of reactively
2026-04-16 22:25:53 +10:00
Krishan b107109453 chore: remove package group definitions from renovate config (#2898)
Removed group definitions for Slatejs and Call.
2026-04-16 22:00:39 +10:00
dependabot[bot] a33e8db9a3 chore(deps): bump dawidd6/action-download-artifact from 16 to 20 (#2880)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 16 to 20.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/2536c51d3d126276eb39f74d6bc9c72ac6ef30d3...8305c0f1062bb0d184d09ef4493ecb9288447732)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '20'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:36:52 +10:00
dependabot[bot] fb76e3ecb4 chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#2893)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:33:37 +10:00
dependabot[bot] 3d79293167 chore(deps): bump softprops/action-gh-release from 2.3.3 to 3.0.0 (#2892)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.3 to 3.0.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/6cbd405e2c4e67a21c47fa9e383d020e4e28b836...b4309332981a82ec1c5618f44dd2e27cc8bfbfda)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:33:01 +10:00
dependabot[bot] 74745edcda chore(deps): bump nginx from 1.29.5-alpine to 1.29.8-alpine (#2894)
Bumps nginx from 1.29.5-alpine to 1.29.8-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-version: 1.29.8-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:31:53 +10:00
dependabot[bot] 0812131a97 chore(deps): bump docker/build-push-action from 6.19.2 to 7.1.0 (#2895)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...bcafcacb16a39f128d818304e6c9c0c18556b85f)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:27:58 +10:00
dependabot[bot] 1068bba5c7 chore(deps): bump docker/login-action from 3.7.0 to 4.1.0 (#2879)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.1.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...4907a6ddec9925e35a0a9e82d7399ccc52663121)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 14:26:52 +10:00
DJ Chase 1b5e58a3b4 chore: add matrixrooms.info to directory list (#2844)
* chore: add matrixrooms.info to directory list

matrixrooms.info is a directory of all public Matrix rooms it can find,
regardless of homeserver. It is much larger than the morg directory,
so is more useful as a general search
2026-03-28 17:35:53 +11:00
Krishan acae043f31 chore: make error more useful and understandable (#2859)
* chore: make error more useful and understandable

* chore: use similar wording
2026-03-27 21:22:46 +11:00
ranidspace b4299f8f37 feat: add YYYY-MM-DD (ISO 8601) date format to presets (#2712)
* Add YYYY-MM-DD (ISO 8601) date format to presets

* Fix formatting due to added date format
2026-03-27 21:20:10 +11:00
20 changed files with 291 additions and 115 deletions
-6
View File
@@ -13,15 +13,9 @@
"matchUpdateTypes": ["lockFileMaintenance"] "matchUpdateTypes": ["lockFileMaintenance"]
}, },
{ {
"groupName": "Slatejs",
"matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"] "matchPackageNames": ["slate", "slate-dom", "slate-history", "slate-react"]
}, },
{ {
"groupName": "Call",
"matchPackageNames": ["@element-hq/element-call-embedded", "matrix-widget-api"]
},
{
"groupName": "Linkify",
"matchPackageNames": ["linkifyjs", "linkify-react"] "matchPackageNames": ["linkifyjs", "linkify-react"]
} }
], ],
+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: ".node-version" node-version-file: ".node-version"
package-manager-cache: false package-manager-cache: false
@@ -25,7 +25,7 @@ jobs:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
run: npm run build run: npm run build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: preview name: preview
path: dist path: dist
@@ -33,7 +33,7 @@ jobs:
- name: Save pr number - name: Save pr number
run: echo ${PR_NUMBER} > ./pr.txt run: echo ${PR_NUMBER} > ./pr.txt
- name: Upload pr number - name: Upload pr number
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: pr name: pr
path: ./pr.txt path: ./pr.txt
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps: steps:
- name: Download pr number - name: Download pr number
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16 uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with: with:
workflow: ${{ github.event.workflow.id }} workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
@@ -25,7 +25,7 @@ jobs:
id: pr id: pr
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
- name: Download artifact - name: Download artifact
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16 uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with: with:
workflow: ${{ github.event.workflow.id }} workflow: ${{ github.event.workflow.id }}
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
- name: Login to Docker Hub #Do not update this action from a outside PR - name: Login to Docker Hub #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -34,7 +34,7 @@ jobs:
- name: Login to the Github Container registry #Do not update this action from a outside PR - name: Login to the Github Container registry #Do not update this action from a outside PR
if: github.event.pull_request.head.repo.fork == false if: github.event.pull_request.head.repo.fork == false
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -50,7 +50,7 @@ jobs:
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
- name: Build Docker image (no push) - name: Build Docker image (no push)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: ".node-version" node-version-file: ".node-version"
package-manager-cache: false package-manager-cache: false
+5 -5
View File
@@ -15,7 +15,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version-file: ".node-version" node-version-file: ".node-version"
package-manager-cache: false package-manager-cache: false
@@ -65,7 +65,7 @@ jobs:
gpg --export | xxd -p gpg --export | xxd -p
echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz
- name: Upload tagged release - name: Upload tagged release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with: with:
tag_name: ${{ steps.vars.outputs.tag }} tag_name: ${{ steps.vars.outputs.tag }}
files: | files: |
@@ -91,12 +91,12 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub #Do not update this action from a outside PR - name: Login to Docker Hub #Do not update this action from a outside PR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Github Container registry #Do not update this action from a outside PR - name: Login to the Github Container registry #Do not update this action from a outside PR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -112,7 +112,7 @@ jobs:
type=raw,value=${{ env.VERSION }} type=raw,value=${{ env.VERSION }}
type=raw,value=latest type=raw,value=latest
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+1 -1
View File
@@ -11,7 +11,7 @@ RUN npm run build
## App ## App
FROM nginx:1.29.5-alpine FROM nginx:1.29.8-alpine
COPY --from=builder /src/dist /app COPY --from=builder /src/dist /app
COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -22,7 +22,7 @@
"#PrivSec.dev:arcticfoxes.net", "#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org" "#disroot:aria-net.org"
], ],
"servers": ["matrix.org", "mozilla.org", "unredacted.org"] "servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
}, },
"hashRouter": { "hashRouter": {
-1
View File
@@ -1,6 +1,5 @@
import React, { ReactNode, useCallback, useRef } from 'react'; import React, { ReactNode, useCallback, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { config } from 'folds';
import { import {
CallEmbedContextProvider, CallEmbedContextProvider,
CallEmbedRefContextProvider, CallEmbedRefContextProvider,
+81 -23
View File
@@ -157,10 +157,12 @@ const getInlineElement = (node: ChildNode, processText: ProcessTextCallback): In
return children; return children;
} }
return node.childNodes.flatMap((child) => getInlineElement(child, processText)); const children = node.childNodes.flatMap((child) => getInlineElement(child, processText));
if (children.length === 0) return [{ text: '' }];
return children;
} }
return []; return [{ text: '' }];
}; };
const parseBlockquoteNode = ( const parseBlockquoteNode = (
@@ -191,7 +193,7 @@ const parseBlockquoteNode = (
if (child.name === 'p') { if (child.name === 'p') {
appendLine(); appendLine();
quoteLines.push(child.children.flatMap((c) => getInlineElement(c, processText))); quoteLines.push(getInlineElement(child, processText));
return; return;
} }
@@ -253,10 +255,67 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
}, },
]; ];
}; };
const parseListNode = (
const parseListMarkdown = (
node: Element, node: Element,
processText: ProcessTextCallback processText: ProcessTextCallback,
): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => { depth = 0
): ParagraphElement[] => {
const md = isTag(node) && node.name === 'ul' ? '*' : '-';
const prefix = node.attribs['data-md'] ?? md;
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
const [digitOrChar] = prefix.match(/^[\da-zA-Z]/) ?? [];
const digit = digitOrChar ? parseInt(digitOrChar, 10) : undefined;
const lines: ParagraphElement[] = [];
let lineNo = digit === undefined || Number.isNaN(digit) ? digitOrChar ?? 1 : digit;
const pushLine = (line: InlineElement[]) => {
lines.push({
type: BlockType.Paragraph,
children: [
{
text: `${Array(depth + 1).join(' ')}${starOrHyphen ? `${starOrHyphen} ` : `${lineNo}. `}`,
},
...line,
],
});
if (typeof lineNo === 'string') {
lineNo = String.fromCharCode(lineNo.charCodeAt(0) + 1);
} else {
lineNo += 1;
}
};
node.children.forEach((child) => {
if (isText(child)) {
pushLine([{ text: processText(child.data) }]);
return;
}
if (isTag(child)) {
if (child.name === 'ul' || child.name === 'ol') {
lines.push(...parseListMarkdown(child, processText, depth + 1));
return;
}
if (child.name === 'li') {
child.children.forEach((c) => {
if (isTag(c) && (c.name === 'ul' || c.name === 'ol')) {
lines.push(...parseListMarkdown(c, processText, depth + 1));
return;
}
pushLine(getInlineElement(c, processText));
});
return;
}
}
pushLine(getInlineElement(child, processText));
});
return lines;
};
const parseListLines = (children: ChildNode[], processText: ProcessTextCallback) => {
const listLines: Array<InlineElement[]> = []; const listLines: Array<InlineElement[]> = [];
let lineHolder: InlineElement[] = []; let lineHolder: InlineElement[] = [];
@@ -267,7 +326,7 @@ const parseListNode = (
lineHolder = []; lineHolder = [];
}; };
node.children.forEach((child) => { children.forEach((child) => {
if (isText(child)) { if (isText(child)) {
lineHolder.push({ text: processText(child.data) }); lineHolder.push({ text: processText(child.data) });
return; return;
@@ -281,7 +340,7 @@ const parseListNode = (
if (child.name === 'li') { if (child.name === 'li') {
appendLine(); appendLine();
listLines.push(child.children.flatMap((c) => getInlineElement(c, processText))); listLines.push(getInlineElement(child, processText));
return; return;
} }
@@ -290,24 +349,23 @@ const parseListNode = (
}); });
appendLine(); appendLine();
const mdSequence = node.attribs['data-md']; return listLines;
if (mdSequence !== undefined) { };
const prefix = mdSequence || '-'; const parseListNode = (
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? []; node: Element,
return listLines.map((lineChildren) => ({ processText: ProcessTextCallback
type: BlockType.Paragraph, ): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
children: [ if (node.attribs['data-md'] !== undefined) {
{ text: `${starOrHyphen ? `${starOrHyphen} ` : `${prefix}. `} ` }, return parseListMarkdown(node, processText);
...lineChildren,
],
}));
} }
const lines = parseListLines(node.childNodes, processText);
if (node.name === 'ol') { if (node.name === 'ol') {
return [ return [
{ {
type: BlockType.OrderedList, type: BlockType.OrderedList,
children: listLines.map((lineChildren) => ({ children: lines.map((lineChildren) => ({
type: BlockType.ListItem, type: BlockType.ListItem,
children: lineChildren, children: lineChildren,
})), })),
@@ -318,7 +376,7 @@ const parseListNode = (
return [ return [
{ {
type: BlockType.UnorderedList, type: BlockType.UnorderedList,
children: listLines.map((lineChildren) => ({ children: lines.map((lineChildren) => ({
type: BlockType.ListItem, type: BlockType.ListItem,
children: lineChildren, children: lineChildren,
})), })),
@@ -329,7 +387,7 @@ const parseHeadingNode = (
node: Element, node: Element,
processText: ProcessTextCallback processText: ProcessTextCallback
): HeadingElement | ParagraphElement => { ): HeadingElement | ParagraphElement => {
const children = node.children.flatMap((child) => getInlineElement(child, processText)); const children = getInlineElement(node, processText);
const headingMatch = node.name.match(/^h([123456])$/); const headingMatch = node.name.match(/^h([123456])$/);
const [, g1AsLevel] = headingMatch ?? ['h3', '3']; const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
@@ -392,7 +450,7 @@ export const domToEditorInput = (
appendLine(); appendLine();
children.push({ children.push({
type: BlockType.Paragraph, type: BlockType.Paragraph,
children: node.children.flatMap((child) => getInlineElement(child, processText)), children: getInlineElement(node, processText),
}); });
return; return;
} }
+2 -2
View File
@@ -11,7 +11,7 @@ import {
} from '../../plugins/markdown'; } from '../../plugins/markdown';
import { findAndReplace } from '../../utils/findAndReplace'; import { findAndReplace } from '../../utils/findAndReplace';
import { sanitizeForRegex } from '../../utils/regex'; import { sanitizeForRegex } from '../../utils/regex';
import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix'; import { isUserId } from '../../utils/matrix';
export type OutputOptions = { export type OutputOptions = {
allowTextFormatting?: boolean; allowTextFormatting?: boolean;
@@ -215,7 +215,7 @@ export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): M
if (node.name === '@room') { if (node.name === '@room') {
mentionData.room = true; mentionData.room = true;
} }
if (isUserId(node.id) && node.id !== mx.getUserId()) { if (isUserId(node.id) && node.id !== mx.getUserId()) {
mentionData.users.add(node.id); mentionData.users.add(node.id);
} }
+17 -6
View File
@@ -23,12 +23,12 @@ import { useAtom, useAtomValue } from 'jotai';
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room';
import { nameInitials } from '../../utils/common'; import { nameInitials } from '../../utils/common';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomUnread } from '../../state/hooks/unread'; import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread'; import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { usePowerLevels } from '../../hooks/usePowerLevels'; import { getPowersLevelFromMatrixEvent, usePowerLevels } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom'; import { copyToClipboard } from '../../utils/dom';
import { markAsRead } from '../../utils/notifications'; import { markAsRead } from '../../utils/notifications';
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
@@ -49,8 +49,8 @@ import {
RoomNotificationMode, RoomNotificationMode,
} from '../../hooks/useRoomsNotificationPreferences'; } from '../../hooks/useRoomsNotificationPreferences';
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
import { useRoomCreators } from '../../hooks/useRoomCreators'; import { getRoomCreatorsForRoomId, useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { getRoomPermissionsAPI, useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { useRoomName } from '../../hooks/useRoomMeta'; import { useRoomName } from '../../hooks/useRoomMeta';
import { useCallMembers, useCallSession } from '../../hooks/useCall'; import { useCallMembers, useCallSession } from '../../hooks/useCall';
@@ -59,6 +59,7 @@ import { callChatAtom } from '../../state/callEmbed';
import { useCallPreferencesAtom } from '../../state/hooks/callPreferences'; import { useCallPreferencesAtom } from '../../state/hooks/callPreferences';
import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
import { livekitSupport } from '../../hooks/useLivekitSupport'; import { livekitSupport } from '../../hooks/useLivekitSupport';
import { StateEvent } from '../../../types/matrix/room';
type RoomNavItemMenuProps = { type RoomNavItemMenuProps = {
room: Room; room: Room;
@@ -287,8 +288,18 @@ export function RoomNavItem({
const autoDiscoveryInfo = useAutoDiscoveryInfo(); const autoDiscoveryInfo = useAutoDiscoveryInfo();
const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => { const handleStartCall: MouseEventHandler<HTMLAnchorElement> = (evt) => {
// Do not join if no livekit support or call is not started by others const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) { const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
const creators = getRoomCreatorsForRoomId(mx, room.roomId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
const hasCallPermission = permissions.event(
StateEvent.GroupCallMemberPrefix,
mx.getSafeUserId()
);
// Do not join if missing permissions or no livekit support and call is not started by others
if (!hasCallPermission || (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0)) {
return; return;
} }
+4
View File
@@ -25,6 +25,10 @@ export const useDateFormatItems = (): DateFormatItem[] =>
format: 'YYYY/MM/DD', format: 'YYYY/MM/DD',
name: 'YYYY/MM/DD', name: 'YYYY/MM/DD',
}, },
{
format: 'YYYY-MM-DD',
name: 'YYYY-MM-DD',
},
{ {
format: '', format: '',
name: 'Custom', name: 'Custom',
+1 -1
View File
@@ -57,7 +57,7 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
return draftPl; return draftPl;
}); });
const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => { export const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
const plContent = mEvent?.getContent<IPowerLevels>(); const plContent = mEvent?.getContent<IPowerLevels>();
const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent); const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent);
+1 -1
View File
@@ -22,7 +22,7 @@ export function SpecVersions({ baseUrl, children }: { baseUrl: string; children:
<Dialog> <Dialog>
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}> <Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
<Text> <Text>
Failed to connect to homeserver. Either homeserver is down or your internet. Unable to connect to the homeserver. The homeserver or your internet connection may be down.
</Text> </Text>
<Button variant="Critical" onClick={retry}> <Button variant="Critical" onClick={retry}>
<Text as="span" size="B400"> <Text as="span" size="B400">
+2 -10
View File
@@ -1,12 +1,5 @@
import { replaceMatch } from '../internal'; import { replaceMatch } from '../internal';
import { import { BlockQuoteRule, CodeBlockRule, ESC_BLOCK_SEQ, HeadingRule, ListRule } from './rules';
BlockQuoteRule,
CodeBlockRule,
ESC_BLOCK_SEQ,
HeadingRule,
OrderedListRule,
UnorderedListRule,
} from './rules';
import { runBlockRule } from './runner'; import { runBlockRule } from './runner';
import { BlockMDParser } from './type'; import { BlockMDParser } from './type';
@@ -23,8 +16,7 @@ export const parseBlockMD: BlockMDParser = (text, parseInline) => {
if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, OrderedListRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, ListRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, UnorderedListRule, parseBlockMD, parseInline);
if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline); if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline);
// replace \n with <br/> because want to preserve empty lines // replace \n with <br/> because want to preserve empty lines
+143 -48
View File
@@ -10,18 +10,22 @@ export const HeadingRule: BlockMDRule = {
}, },
}; };
const CODEBLOCK_MD_1 = '```'; // opening fence: 3 or more backticks
const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((?:.*\n)+?)`{3} *(?!.)\n?/m; // capture the exact fence length in group 1
// optional info string in group 2
// code content in group 3
// closing fence must match the exact same fence sequence via \1
const CODEBLOCK_REG_1 = /^(`{3,})(?!`)(\S*)\n((?:.*\n)+?)\1 *(?!.)\n?/m;
export const CodeBlockRule: BlockMDRule = { export const CodeBlockRule: BlockMDRule = {
match: (text) => text.match(CODEBLOCK_REG_1), match: (text) => text.match(CODEBLOCK_REG_1),
html: (match) => { html: (match) => {
const [, g1, g2] = match; const [, fence, g1, g2] = match;
// use last identifier after dot, e.g. for "example.json" gets us "json" as language code. // use last identifier after dot, e.g. for "example.json" gets us "json" as language code.
const langCode = g1 ? g1.substring(g1.lastIndexOf('.') + 1) : null; const langCode = g1 ? g1.substring(g1.lastIndexOf('.') + 1) : null;
const filename = g1 !== langCode ? g1 : null; const filename = g1 !== langCode ? g1 : null;
const classNameAtt = langCode ? ` class="language-${langCode}"` : ''; const classNameAtt = langCode ? ` class="language-${langCode}"` : '';
const filenameAtt = filename ? ` data-label="${filename}"` : ''; const filenameAtt = filename ? ` data-label="${filename}"` : '';
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}${filenameAtt}>${g2}</code></pre>`; return `<pre data-md="${fence}"><code${classNameAtt}${filenameAtt}>${g2}</code></pre>`;
}, },
}; };
@@ -48,55 +52,146 @@ export const BlockQuoteRule: BlockMDRule = {
}; };
const ORDERED_LIST_MD_1 = '-'; const ORDERED_LIST_MD_1 = '-';
const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
const O_LIST_START = /^([\d])\./;
const O_LIST_TYPE = /^([aAiI])\./;
const O_LIST_TRAILING_NEWLINE = /\n$/;
const ORDERED_LIST_REG_1 = /(^(?:-|[\da-zA-Z]\.) +.+\n?)+/m;
export const OrderedListRule: BlockMDRule = {
match: (text) => text.match(ORDERED_LIST_REG_1),
html: (match, parseInline) => {
const [listText] = match;
const [, listStart] = listText.match(O_LIST_START) ?? [];
const [, listType] = listText.match(O_LIST_TYPE) ?? [];
const lines = listText
.replace(O_LIST_TRAILING_NEWLINE, '')
.split('\n')
.map((lineText) => {
const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
const txt = parseInline ? parseInline(line) : line;
return `<li><p>${txt}</p></li>`;
})
.join('');
const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
const startAtt = listStart ? ` start="${listStart}"` : '';
const typeAtt = listType ? ` type="${listType}"` : '';
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>${lines}</ol>`;
},
};
const UNORDERED_LIST_MD_1 = '*'; const UNORDERED_LIST_MD_1 = '*';
const U_LIST_ITEM_PREFIX = /^\* */; const LIST_ITEM_REG = /^( *)([-*]|[\da-zA-Z]\.) +(.+)$/;
const U_LIST_TRAILING_NEWLINE = /\n$/; type ListType = 'ol' | 'ul';
const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
export const UnorderedListRule: BlockMDRule = { function getListType(marker: string): ListType {
match: (text) => text.match(UNORDERED_LIST_REG_1), return marker === '*' ? 'ul' : 'ol';
}
function getOrderedMeta(marker: string) {
const startMatch = marker.match(/^(\d)\./);
const typeMatch = marker.match(/^([aAiI])\./);
return {
start: startMatch?.[1],
type: typeMatch?.[1],
};
}
interface ParsedLine {
indent: number;
marker: string;
content: string;
listType: ListType;
}
function parseLines(text: string): ParsedLine[] {
return text
.replace(/\n$/, '')
.split('\n')
.map((line) => {
const match = line.match(LIST_ITEM_REG);
if (!match) return null;
const [, spaces, marker, content] = match;
return {
indent: spaces.length,
marker,
content,
listType: getListType(marker),
};
})
.filter(Boolean) as ParsedLine[];
}
function openList(line: ParsedLine) {
if (line.listType === 'ul') {
return `<ul data-md="${UNORDERED_LIST_MD_1}">`;
}
const { type, start } = getOrderedMeta(line.marker);
const dataMdAtt = `data-md="${type || start || ORDERED_LIST_MD_1}"`;
const startAtt = start ? ` start="${start}"` : '';
const typeAtt = type ? ` type="${type}"` : '';
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>`;
}
function closeList(listType: ListType) {
return listType === 'ul' ? '</ul>' : '</ol>';
}
function buildList(lines: ParsedLine[], parseInline?: (s: string) => string): string {
let html = '';
const stack: ('ul' | 'ol')[] = [];
lines.forEach((line, index) => {
const prev = lines[index - 1];
const next = lines[index + 1];
const content = parseInline ? parseInline(line.content) : line.content;
// FIRST ITEM
if (!prev) {
html += openList(line);
stack.push(line.listType);
}
// DEEPER INDENT > open nested list
else if (line.indent > prev.indent) {
html += openList(line);
stack.push(line.listType);
}
// SAME LEVEL
else if (line.indent === prev.indent) {
html += '</li>';
// different list type
if (line.listType !== prev.listType) {
html += closeList(stack.pop()!);
html += openList(line);
stack.push(line.listType);
}
}
// GOING BACK UP
else if (line.indent < prev.indent) {
html += '</li>';
while (stack.length > line.indent + 1) {
html += closeList(stack.pop()!);
html += '</li>';
}
if (line.listType !== stack[stack.length - 1]) {
html += closeList(stack.pop()!);
html += openList(line);
stack.push(line.listType);
}
}
html += `<li><p>${content}</p>`;
// LAST ITEM cleanup
if (!next) {
html += '</li>';
while (stack.length) {
html += closeList(stack.pop()!);
}
}
});
return html;
}
const LIST_REG_1 = /^(?: *(?:[-*]|[\da-zA-Z]\.) +.+\n?)+/m;
export const ListRule: BlockMDRule = {
match: (text) => text.match(LIST_REG_1),
html: (match, parseInline) => { html: (match, parseInline) => {
const [listText] = match; const [listText] = match;
const lines = listText const lines = parseLines(listText);
.replace(U_LIST_TRAILING_NEWLINE, '')
.split('\n')
.map((lineText) => {
const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
const txt = parseInline ? parseInline(line) : line;
return `<li><p>${txt}</p></li>`;
})
.join('');
return `<ul data-md="${UNORDERED_LIST_MD_1}">${lines}</ul>`; const html = buildList(lines, parseInline);
return html;
}, },
}; };
+7 -1
View File
@@ -1,7 +1,13 @@
import { atom } from 'jotai'; import { atom } from 'jotai';
const STORAGE_KEY = 'settings'; const STORAGE_KEY = 'settings';
export type DateFormat = 'D MMM YYYY' | 'DD/MM/YYYY' | 'MM/DD/YYYY' | 'YYYY/MM/DD' | ''; export type DateFormat =
| 'D MMM YYYY'
| 'DD/MM/YYYY'
| 'MM/DD/YYYY'
| 'YYYY/MM/DD'
| 'YYYY-MM-DD'
| '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500'; export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export enum MessageLayout { export enum MessageLayout {
Modern = 0, Modern = 0,
+11
View File
@@ -120,12 +120,23 @@ export const CodeBlockBottomShadow = style({
background: `linear-gradient(to top, #00000022, #00000000)`, background: `linear-gradient(to top, #00000022, #00000000)`,
}); });
const BaseList = style({});
export const List = style([ export const List = style([
BaseList,
DefaultReset, DefaultReset,
MarginSpaced, MarginSpaced,
{ {
padding: `0 ${config.space.S100}`, padding: `0 ${config.space.S100}`,
paddingLeft: config.space.S600, paddingLeft: config.space.S600,
selectors: {
'& &': {
marginTop: config.space.S200,
marginBottom: config.space.S200,
},
'li:last-child &': {
marginBottom: 0,
},
},
}, },
]); ]);
+6
View File
@@ -129,3 +129,9 @@ textarea {
audio:not([controls]) { audio:not([controls]) {
display: none !important; display: none !important;
} }
/* Fix Firefox rendering lists that have empty items with those items collapsed in on eachother */
li p::before {
content: '';
display: inline-block;
}