Compare commits

..

24 Commits
v4.11.1 ... dev

Author SHA1 Message Date
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](2536c51d3d...8305c0f106)

---
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](bbbca2ddaa...043fb46d1a)

---
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](6cbd405e2c...b430933298)

---
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](10e90e3645...bcafcacb16)

---
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](c94ce9fb46...4907a6ddec)

---
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
Krishan
b6adac6714 chore: add notice about SDK replacement (#2778) 2026-03-25 12:10:15 +11:00
DJ Chase
1c8f203164 chore: add 'Stickers and Emojis' as featured space (#2842)
* Mention CLA in CONTRIBUTING.md

Closes: #2146

* add: 'Stickers and Emojis' to config.json

Add #stickers-and-emojis:tastytea.de (space) to config.json
2026-03-25 12:09:16 +11:00
Krishan
0c30ece281 fix: remove typo in no rooms UI (#2834) 2026-03-23 16:57:52 +11:00
Krishan
4e559e56d4 chore: group related package update together (#2833) 2026-03-23 16:49:22 +11:00
Krishan
19f28b40ac chore: use private vulnerability disclosure (#2827) 2026-03-22 18:29:09 +11:00
Krishan
bcaf43a540 chore: fix link in issue triage template (#2826)
* chore: fix link in issue triage template

* chore: delete .github/PULL_REQUEST_TEMPLATE.md
2026-03-22 18:20:33 +11:00
Krishan
9c7b635e7e chore: add new issue triage discussion template (#2825)
* chore: add new issue triage discussion template

* chore: ask for desktop app version as well

* chore: create preapproved.md
2026-03-22 17:55:53 +11:00
Krishan
65c87dff3a chore: add git author to the sem release (#2815) 2026-03-21 12:07:52 +11:00
Krishan
132a76df27 chore: add semantic release (#2759)
* chore: install deps related to semantic release

* chore: add husky config

* ci: add a script to update version number on new release

* ci: update ci/cd to include semantic release changes

* chore: merge dev to semantic-release
2026-03-19 16:26:25 +11:00
DJ Chase
b0954eeddc fix: Mention CLA in CONTRIBUTING.md (#2804)
Mention CLA in CONTRIBUTING.md

Closes: #2146
2026-03-19 11:41:41 +11:00
Ajay Bura
8f1add6059 fix: prevent codeblock filename drop on edit (#2780)
prevent codeblock filename drop on edit
2026-03-15 15:37:14 +11:00
Jan Jurzitza
8a78c9699e feat: allow using filenames in codeblocks (#2455)
Allow using filenames in codeblocks

- If there is a dot in the language name, we instead treat the first line after ``` as the filename and everything after the last dot as the language
- we use a custom "data-label" attribute on the code block to specify the name of the file (so only compatible with cinny from this point onwards)
2026-03-14 18:54:03 +11:00
Krishan
0721b29a2c chore: batch slate related deps (#2775) 2026-03-14 17:22:57 +11:00
Ajay Bura
3d354909d6 fix: hover state on url preview image and make it keyboard friendly (#2777)
add hover state on url preview image and make it keyboard friendly
2026-03-14 17:22:18 +11:00
LeaPhant
7570a84dfd Show image viewer when clicking url preview thumbnail (#2309)
* Show large image overlay when clicking url preview thumbnail

* Move image overlay into its own component

* Move ImageOverlay props into extended type

* Remove export for internal type
2026-03-14 11:04:55 +05:30
31 changed files with 7488 additions and 186 deletions

View File

@@ -0,0 +1,127 @@
labels: ["needs-confirmation"]
body:
- type: markdown #add faqs in future
attributes:
value: |
> [!IMPORTANT]
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
- type: markdown
attributes:
value: "# Issue Details"
- type: textarea
attributes:
label: Issue Description
description: |
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
placeholder: |
When I try to send a message in a room, the message doesn't appear in the timeline.
OR
The application crashes when I click on the settings button.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: |
Describe how you expect Cinny to behave in this situation.
placeholder: |
I expected the message to appear in the room timeline immediately after sending.
OR
The settings panel should open smoothly without any crashes.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: |
Describe how Cinny actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
placeholder: |
The application freezes for 3 seconds and then shows a white screen.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Provide a detailed set of step-by-step instructions for reproducing this issue.
placeholder: |
1. Open Cinny and log in to my account
2. Navigate to the #general room
3. Type a message in the message box
4. Press Enter to send
5. Notice that the message doesn't appear in the timeline
validations:
required: true
- type: textarea
attributes:
label: Environement
description: |
Please provide information about your environment. Include the following:
- OS:
- Browser:
- Cinny Web Version: (app.cinny.in or self hosted)
- Cinny desktop Version: (appimage or deb or flatpak)
- Matrix Homeserver:
placeholder: |
- OS: Windows 11
- Browser: Chrome 120.0.6099.109
- Cinny Web Version: 3.2.0 (app.cinny.in or self hosted)
- Cinny desktop Version: 3.2.0 (appimage or deb or flatpak)
- Matrix Homeserver: matrix.org (Synapse 1.97.0)
render: text
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: |
If applicable, add browser console logs to help explain your problem.
**To get browser console logs:**
- Chrome/Edge: Press F12 → Console tab
- Firefox: Press F12 → Console tab
- Safari: Develop → Show Web Inspector → Console
Please wrap large log outputs in code blocks with triple backticks (```).
placeholder: |
```
Error: Failed to send message
at MessageComposer.sendMessage (composer.js:245)
at HTMLButtonElement.onClick (composer.js:189)
TypeError: Cannot read property 'content' of undefined
at RoomTimeline.render (timeline.js:567)
```
render: shell
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Add any other context about the problem here (e.g., when did this start happening, does it happen on different homeservers, etc.)
placeholder: |
- This started happening after I updated to version 3.2.0
- It only happens in encrypted rooms, not in public rooms
- I've tried on both Firefox and Chrome with the same result
- It works fine on my phone using the same account
- This happens on all homeservers I've tested (matrix.org, mozilla.org)
validations:
required: false
- type: markdown
attributes:
value: |
# User Acknowledgements
> [!TIP]
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
- type: checkboxes #add faqs in future
attributes:
label: "I acknowledge that:"
options:
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required: true
- label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
required: true

View File

@@ -1,57 +0,0 @@
name: 🐞 Bug Report
description: Report a bug
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/ajbura/cinny/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Browser: [e.g. chrome 99.5, firefox 97.2]
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
4. Matrix homeserver: [e.g. matrix.org]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.

View File

@@ -1,4 +1,5 @@
blank_issues_enabled: false
contact_links: contact_links:
- name: 💬 Matrix Chat - name: Features, Bug Reports, Questions
url: https://matrix.to/#/#cinny:matrix.org url: https://github.com/cinnyapp/cinny/discussions/new/choose
about: Ask questions and talk to other Cinny users and the maintainers about: Our preferred starting point if you have any questions or suggestions about features or behavior.

View File

@@ -1,33 +0,0 @@
name: 💡 Feature Request
description: Suggest an idea
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.

9
.github/ISSUE_TEMPLATE/preapproved.md vendored Normal file
View File

@@ -0,0 +1,9 @@
---
name: Pre-Discussed and Approved Topics
about: |-
Only for topics already discussed and approved in the GitHub Discussions section.
---
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**

View File

@@ -1,22 +0,0 @@
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings

3
.github/SECURITY.md vendored
View File

@@ -1,3 +0,0 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**

16
.github/renovate.json vendored
View File

@@ -3,12 +3,26 @@
"extends": [ "extends": [
"config:recommended", "config:recommended",
":dependencyDashboardApproval", ":dependencyDashboardApproval",
":semanticCommits" ":semanticCommits",
"group:monorepos"
], ],
"labels": ["Dependencies"], "labels": ["Dependencies"],
"rebaseWhen": "conflicted",
"packageRules": [ "packageRules": [
{ {
"matchUpdateTypes": ["lockFileMaintenance"] "matchUpdateTypes": ["lockFileMaintenance"]
},
{
"groupName": "Slatejs",
"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"]
} }
], ],
"lockFileMaintenance": { "lockFileMaintenance": {

View File

@@ -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

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 }}

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

View File

@@ -1,16 +1,19 @@
name: Production deploy name: Production deploy
on: on:
release: workflow_dispatch:
types: [published]
jobs: jobs:
deploy-and-tarball: deploy-and-tarball:
name: Netlify deploy and tarball name: Netlify deploy and tarball
outputs:
version: ${{ steps.vars.outputs.tag }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup node - name: Setup node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
@@ -18,6 +21,19 @@ jobs:
package-manager-cache: false package-manager-cache: false
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run semantic release
run: npm run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: Get version from tag
id: vars
run: |
TAG=$(git describe --tags --abbrev=0)
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Build app - name: Build app
env: env:
NODE_OPTIONS: '--max_old_space_size=4096' NODE_OPTIONS: '--max_old_space_size=4096'
@@ -26,7 +42,7 @@ jobs:
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0 uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
with: with:
publish-dir: dist publish-dir: dist
deploy-message: 'Prod deploy ${{ github.ref_name }}' deploy-message: 'Prod deploy ${{ steps.vars.outputs.tag }}'
enable-commit-comment: false enable-commit-comment: false
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
production-deploy: true production-deploy: true
@@ -36,9 +52,6 @@ jobs:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
timeout-minutes: 1 timeout-minutes: 1
- name: Get version from tag
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Create tar.gz - name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Sign tar.gz - name: Sign tar.gz
@@ -52,14 +65,18 @@ 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 }}
files: | files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz cinny-${{ steps.vars.outputs.tag }}.tar.gz
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
publish-image: publish-image:
name: Push Docker image to Docker Hub, GHCR name: Push Docker image to Docker Hub, GHCR
needs: deploy-and-tarball
env:
VERSION: ${{ needs.deploy-and-tarball.outputs.version }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@@ -67,17 +84,19 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- 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 }}
@@ -89,11 +108,14 @@ jobs:
images: | images: |
${{ secrets.DOCKER_USERNAME }}/cinny ${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ env.VERSION }}
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
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

3
.husky/pre-commit Normal file
View File

@@ -0,0 +1,3 @@
# These are commented until we enable lint and typecheck
# npx tsc -p tsconfig.json --noEmit
# npx lint-staged

View File

@@ -18,7 +18,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
## Pull requests ## Pull requests
> ### Legal Notice > ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap. **NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.

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

View File

@@ -16,6 +16,10 @@ A Matrix client focusing primarily on simple, elegant and secure interface. The
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1) - [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
- [Contributing](./CONTRIBUTING.md) - [Contributing](./CONTRIBUTING.md)
> [!IMPORTANT]
We are currently in the [process of replacing the matrix-js-sdk](https://github.com/cinnyapp/cinny/issues/257#issuecomment-3714406704) with our own SDK. As a result, we will not be accepting any pull requests until further notice.
Thank you for your understanding.
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380"> <img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
## Getting started ## Getting started

View File

@@ -11,7 +11,8 @@
"#space:unredacted.org", "#space:unredacted.org",
"#science-space:matrix.org", "#science-space:matrix.org",
"#libregaming-games:tchncs.de", "#libregaming-games:tchncs.de",
"#mathematics-on:matrix.org" "#mathematics-on:matrix.org",
"#stickers-and-emojis:tastytea.de"
], ],
"rooms": [ "rooms": [
"#cinny:matrix.org", "#cinny:matrix.org",
@@ -21,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": {

7104
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,52 @@
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "yarn check:eslint && yarn check:prettier", "lint": "npm run check:eslint && npm run check:prettier",
"check:eslint": "eslint src/*", "check:eslint": "eslint src/*",
"check:prettier": "prettier --check .", "check:prettier": "prettier --check .",
"fix:prettier": "prettier --write .", "fix:prettier": "prettier --write .",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"prepare": "husky install",
"commit": "git-cz",
"semantic-release": "semantic-release"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": "eslint",
"*": "prettier --ignore-unknown --write"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"release": {
"branches": [
"dev"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec",
{
"prepareCmd": "node scripts/update-version.js ${nextRelease.version}"
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"package-lock.json",
"src/app/features/settings/about/About.tsx",
"src/app/pages/auth/AuthFooter.tsx",
"src/app/pages/client/WelcomePage.tsx"
],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
"@semantic-release/github"
]
}, },
"keywords": [], "keywords": [],
"author": "Ajay Bura", "author": "Ajay Bura",
@@ -82,6 +123,8 @@
"@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@rollup/plugin-inject": "5.0.3", "@rollup/plugin-inject": "5.0.3",
"@rollup/plugin-wasm": "6.1.1", "@rollup/plugin-wasm": "6.1.1",
"@semantic-release/exec": "7.1.0",
"@semantic-release/git": "10.0.1",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.1",
"@types/file-saver": "2.0.5", "@types/file-saver": "2.0.5",
"@types/is-hotkey": "0.1.10", "@types/is-hotkey": "0.1.10",
@@ -96,6 +139,7 @@
"@typescript-eslint/parser": "5.46.1", "@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "4.2.0", "@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3", "buffer": "6.0.3",
"cz-conventional-changelog": "3.3.0",
"eslint": "8.29.0", "eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
@@ -103,7 +147,10 @@
"eslint-plugin-jsx-a11y": "6.6.1", "eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.11", "eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"husky": "9.1.7",
"lint-staged": "16.3.2",
"prettier": "2.8.1", "prettier": "2.8.1",
"semantic-release": "25.0.3",
"typescript": "4.9.4", "typescript": "4.9.4",
"vite": "5.4.19", "vite": "5.4.19",
"vite-plugin-pwa": "0.20.5", "vite-plugin-pwa": "0.20.5",

48
scripts/update-version.js Normal file
View File

@@ -0,0 +1,48 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const version = process.argv[2];
if (!version) {
console.error("Version argument missing");
process.exit(1);
}
const root = path.resolve(__dirname, "..");
const newVersionTag = `v${version}`;
// Update package.json + package-lock.json safely
execSync(`npm version ${version} --no-git-tag-version`, {
cwd: root,
stdio: "inherit",
});
console.log(`Updated package.json and package-lock.json → ${version}`);
// Update UI version references
const files = [
"src/app/features/settings/about/About.tsx",
"src/app/pages/auth/AuthFooter.tsx",
"src/app/pages/client/WelcomePage.tsx",
];
files.forEach((filePath) => {
const absPath = path.join(root, filePath);
if (!fs.existsSync(absPath)) {
console.warn(`File not found: ${filePath}`);
return;
}
const content = fs.readFileSync(absPath, "utf8");
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
fs.writeFileSync(absPath, updated);
console.log(`Updated ${filePath}${newVersionTag}`);
});

View File

@@ -0,0 +1,45 @@
import FocusTrap from 'focus-trap-react';
import { as, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import React, { ReactNode } from 'react';
import { ModalWide } from '../styles/Modal.css';
import { stopPropagation } from '../utils/keyboard';
export type RenderViewerProps = {
src: string;
alt: string;
requestClose: () => void;
};
type ImageOverlayProps = RenderViewerProps & {
viewer: boolean;
renderViewer: (props: RenderViewerProps) => ReactNode;
};
export const ImageOverlay = as<'div', ImageOverlayProps>(
({ src, alt, viewer, requestClose, renderViewer, ...props }, ref) => (
<Overlay {...props} ref={ref} open={viewer} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => requestClose(),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
className={ModalWide}
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
{renderViewer({
src,
alt,
requestClose,
})}
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)
);

View File

@@ -228,9 +228,13 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
children: [{ text }], children: [{ text }],
})); }));
const childCode = node.children[0]; const childCode = node.children[0];
const className = const attribs =
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : ''; isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs : undefined;
const prefix = { text: `${mdSequence}${className.replace('language-', '')}` }; const languageClass = attribs?.class;
const customLabel = attribs?.['data-label'];
const prefix = {
text: `${mdSequence}${customLabel ?? languageClass?.replace('language-', '') ?? ''}`,
};
const suffix = { text: mdSequence }; const suffix = { text: mdSequence };
return [ return [
{ type: BlockType.Paragraph, children: [prefix] }, { type: BlockType.Paragraph, children: [prefix] },

View File

@@ -23,6 +23,11 @@ export const UrlPreviewImg = style([
objectPosition: 'center', objectPosition: 'center',
flexShrink: 0, flexShrink: 0,
overflow: 'hidden', overflow: 'hidden',
cursor: 'pointer',
':hover': {
filter: 'brightness(0.8)',
},
}, },
]); ]);

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { IPreviewUrlResponse } from 'matrix-js-sdk'; import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds'; import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
import { ImageOverlay } from '../ImageOverlay';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview'; import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -12,6 +13,8 @@ import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom'; import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix'; import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { ImageViewer } from '../image-viewer';
import { onEnterOrSpace } from '../../utils/keyboard';
const linkStyles = { color: color.Success.Main }; const linkStyles = { color: color.Success.Main };
@@ -19,6 +22,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
({ url, ts, ...props }, ref) => { ({ url, ts, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState(false);
const [previewStatus, loadPreview] = useAsyncCallback( const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]) useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
); );
@@ -30,7 +34,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null; if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => { const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp( const thumbUrl = mxcUrlToHttp(
mx, mx,
prev['og:image'] || '', prev['og:image'] || '',
useAuthentication, useAuthentication,
@@ -40,9 +44,31 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
false false
); );
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
return ( return (
<> <>
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />} {thumbUrl && (
<UrlPreviewImg
src={thumbUrl}
alt={prev['og:title']}
title={prev['og:title']}
tabIndex={0}
onKeyDown={(evt) => onEnterOrSpace(() => setViewer(true))(evt)}
onClick={() => setViewer(true)}
/>
)}
{imgUrl && (
<ImageOverlay
src={imgUrl}
alt={prev['og:title']}
viewer={viewer}
requestClose={() => {
setViewer(false);
}}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
<UrlPreviewContent> <UrlPreviewContent>
<Text <Text
style={linkStyles} style={linkStyles}

View File

@@ -286,7 +286,7 @@ export function Search({ requestClose }: SearchProps) {
gap="100" gap="100"
> >
<Text size="H6" align="Center"> <Text size="H6" align="Center">
{result ? 'No Match Found' : `No Rooms'}`} {result ? 'No Match Found' : 'No Rooms'}
</Text> </Text>
<Text size="T200" align="Center"> <Text size="T200" align="Center">
{result {result

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',

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">

View File

@@ -16,8 +16,12 @@ 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 [, g1, g2] = match;
const classNameAtt = g1 ? ` class="language-${g1}"` : ''; // use last identifier after dot, e.g. for "example.json" gets us "json" as language code.
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}>${g2}</code></pre>`; const langCode = g1 ? g1.substring(g1.lastIndexOf('.') + 1) : null;
const filename = g1 !== langCode ? g1 : null;
const classNameAtt = langCode ? ` class="language-${langCode}"` : '';
const filenameAtt = filename ? ` data-label="${filename}"` : '';
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}${filenameAtt}>${g2}</code></pre>`;
}, },
}; };

View File

@@ -232,8 +232,9 @@ export function CodeBlock({
opts: HTMLReactParserOptions; opts: HTMLReactParserOptions;
}) { }) {
const code = children[0]; const code = children[0];
const languageClass = const attribs = code instanceof Element && code.name === 'code' ? code.attribs : undefined;
code instanceof Element && code.name === 'code' ? code.attribs.class : undefined; const languageClass = attribs?.class;
const customLabel = attribs?.['data-label'];
const language = const language =
languageClass && languageClass.startsWith('language-') languageClass && languageClass.startsWith('language-')
? languageClass.replace('language-', '') ? languageClass.replace('language-', '')
@@ -262,7 +263,7 @@ export function CodeBlock({
<Header variant="Surface" size="400" className={css.CodeBlockHeader}> <Header variant="Surface" size="400" className={css.CodeBlockHeader}>
<Box grow="Yes"> <Box grow="Yes">
<Text size="L400" truncate> <Text size="L400" truncate>
{language ?? 'Code'} {customLabel ?? language ?? 'Code'}
</Text> </Text>
</Box> </Box>
<Box shrink="No" gap="200"> <Box shrink="No" gap="200">

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,

View File

@@ -71,7 +71,7 @@ const permittedTagToAttributes = {
ul: ['data-md'], ul: ['data-md'],
a: ['name', 'target', 'href', 'rel', 'data-md'], a: ['name', 'target', 'href', 'rel', 'data-md'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'], img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
code: ['class', 'data-md'], code: ['class', 'data-md', 'data-label'],
strong: ['data-md'], strong: ['data-md'],
i: ['data-md'], i: ['data-md'],
em: ['data-md'], em: ['data-md'],