├── .github
└── workflows
│ ├── docker.yaml
│ ├── helm.yaml
│ └── k8s.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── build.sh
├── etc
└── nginx.conf
├── helm
├── cerulean
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── hpa.yaml
│ │ ├── ingress.yaml
│ │ ├── service.yaml
│ │ ├── serviceaccount.yaml
│ │ └── tests
│ │ │ └── test-connection.yaml
│ └── values.yaml
├── cr.yaml
└── ct.yaml
├── package.json
├── public
├── Inter.ttf
├── chevron.svg
├── close.svg
├── delete.svg
├── favicon.ico
├── filter.svg
├── icon.svg
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
├── send-active.svg
├── send.svg
├── thread-corner.svg
└── thread-line.svg
├── src
├── App.css
├── App.js
├── Client.js
├── Client.test.js
├── ClientContext.js
├── InputPost.css
├── InputPost.js
├── Message.css
├── Message.js
├── Modal.js
├── Reputation.js
├── Reputation.test.js
├── ReputationList.js
├── ReputationPane.css
├── ReputationPane.js
├── StatusPage.css
├── StatusPage.js
├── TimelinePage.css
├── TimelinePage.js
├── UserPage.css
├── UserPage.js
├── index.js
├── routing.js
└── setupTests.js
└── yarn.lock
/.github/workflows/docker.yaml:
--------------------------------------------------------------------------------
1 | # Based on https://github.com/docker/build-push-action
2 |
3 | name: "Docker"
4 |
5 | on:
6 | push:
7 | branches: ["main"]
8 | release: # A GitHub release was published
9 | types: [published]
10 | workflow_dispatch: # A build was manually requested
11 | workflow_call: # Another pipeline called us
12 |
13 | env:
14 | GHCR_NAMESPACE: matrix-org
15 | PLATFORMS: linux/amd64
16 |
17 | jobs:
18 | cerulean:
19 | name: Cerulean image
20 | runs-on: ubuntu-latest
21 | permissions:
22 | contents: read
23 | packages: write
24 | security-events: write # To upload Trivy sarif files
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 | - name: Set up QEMU
29 | uses: docker/setup-qemu-action@v1
30 | - name: Set up Docker Buildx
31 | uses: docker/setup-buildx-action@v2
32 | - name: Login to GitHub Containers
33 | uses: docker/login-action@v2
34 | with:
35 | registry: ghcr.io
36 | username: ${{ github.repository_owner }}
37 | password: ${{ secrets.GITHUB_TOKEN }}
38 |
39 | - name: Build main Cerulean image
40 | if: github.ref_name == 'main'
41 | id: docker_build_cerulean
42 | uses: docker/build-push-action@v3
43 | with:
44 | cache-from: type=gha
45 | cache-to: type=gha,mode=max
46 | context: .
47 | platforms: ${{ env.PLATFORMS }}
48 | push: true
49 | tags: |
50 | ghcr.io/${{ env.GHCR_NAMESPACE }}/cerulean:main
51 |
52 | - name: Build release Cerulean image
53 | if: github.event_name == 'release' # Only for GitHub releases
54 | id: docker_build_cerulean_release
55 | uses: docker/build-push-action@v3
56 | with:
57 | cache-from: type=gha
58 | cache-to: type=gha,mode=max
59 | context: .
60 | platforms: ${{ env.PLATFORMS }}
61 | push: true
62 | tags: |
63 | ghcr.io/${{ env.GHCR_NAMESPACE }}/cerulean:latest
64 | ghcr.io/${{ env.GHCR_NAMESPACE }}/cerulean:${{ env.RELEASE_VERSION }}
65 |
66 | - name: Run Trivy vulnerability scanner
67 | uses: aquasecurity/trivy-action@master
68 | if: github.ref_name == 'main'
69 | with:
70 | image-ref: ghcr.io/${{ env.GHCR_NAMESPACE }}/cerulean:main
71 | format: "sarif"
72 | output: "trivy-results.sarif"
73 |
74 | - name: Upload Trivy scan results to GitHub Security tab
75 | if: github.ref_name == 'main'
76 | uses: github/codeql-action/upload-sarif@v2
77 | with:
78 | sarif_file: "trivy-results.sarif"
--------------------------------------------------------------------------------
/.github/workflows/helm.yaml:
--------------------------------------------------------------------------------
1 | name: Release Charts
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - 'helm/**' # only execute if we have helm chart changes
9 |
10 | jobs:
11 | release:
12 | # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
13 | # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
14 | permissions:
15 | contents: write
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Configure Git
24 | run: |
25 | git config user.name "$GITHUB_ACTOR"
26 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
27 |
28 | - name: Install Helm
29 | uses: azure/setup-helm@v3
30 | with:
31 | version: v3.10.0
32 |
33 | - name: Run chart-releaser
34 | uses: helm/chart-releaser-action@v1.4.1
35 | env:
36 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
37 | with:
38 | config: helm/cr.yaml
39 | charts_dir: helm/
40 |
--------------------------------------------------------------------------------
/.github/workflows/k8s.yaml:
--------------------------------------------------------------------------------
1 | name: k8s
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | paths:
7 | - 'helm/**' # only execute if we have helm chart changes
8 | pull_request:
9 | branches: ["main"]
10 | paths:
11 | - 'helm/**'
12 |
13 | jobs:
14 | lint:
15 | name: Lint Helm chart
16 | runs-on: ubuntu-latest
17 | outputs:
18 | changed: ${{ steps.list-changed.outputs.changed }}
19 | steps:
20 | - uses: actions/checkout@v3
21 | with:
22 | fetch-depth: 0
23 | - uses: azure/setup-helm@v3
24 | with:
25 | version: v3.10.0
26 | - uses: actions/setup-python@v4
27 | with:
28 | python-version: 3.11
29 | check-latest: true
30 | - uses: helm/chart-testing-action@v2.3.1
31 | - name: Get changed status
32 | id: list-changed
33 | run: |
34 | changed=$(ct list-changed --config helm/ct.yaml --target-branch ${{ github.event.repository.default_branch }})
35 | if [[ -n "$changed" ]]; then
36 | echo "::set-output name=changed::true"
37 | fi
38 |
39 | - name: Run lint
40 | run: ct lint --config helm/ct.yaml
41 |
42 | # only bother to run if lint step reports a change to the helm chart
43 | install:
44 | needs:
45 | - lint
46 | if: ${{ needs.lint.outputs.changed == 'true' }}
47 | name: Install Helm charts
48 | runs-on: ubuntu-latest
49 | steps:
50 | - name: Checkout
51 | uses: actions/checkout@v3
52 | with:
53 | fetch-depth: 0
54 | ref: ${{ inputs.checkoutCommit }}
55 | - name: Install Kubernetes tools
56 | uses: yokawasa/action-setup-kube-tools@v0.8.2
57 | with:
58 | setup-tools: |
59 | helmv3
60 | helm: "3.10.3"
61 | - uses: actions/setup-python@v4
62 | with:
63 | python-version: "3.10"
64 | - name: Set up chart-testing
65 | uses: helm/chart-testing-action@v2.3.1
66 | - name: Create k3d cluster
67 | uses: nolar/setup-k3d-k3s@v1
68 | with:
69 | version: v1.21
70 | - name: Remove node taints
71 | run: |
72 | kubectl taint --all=true nodes node.cloudprovider.kubernetes.io/uninitialized- || true
73 | - name: Run chart-testing (install)
74 | run: ct install --config helm/ct.yaml
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-alpine AS builder
2 | WORKDIR /app
3 | COPY package.json ./
4 | COPY yarn.lock ./
5 | RUN --mount=type=cache,target=/root/.yarn \
6 | YARN_CACHE_FOLDER=/root/.yarn \
7 | yarn install --frozen-lockfile
8 | COPY . .
9 | RUN --mount=type=cache,target=/root/.yarn \
10 | YARN_CACHE_FOLDER=/root/.yarn \
11 | yarn build
12 |
13 | FROM nginx:1.22-alpine AS server
14 | COPY ./etc/nginx.conf /etc/nginx/conf.d/default.conf
15 | COPY --from=builder ./app/build /usr/share/nginx/html
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cerulean
2 |
3 | Cerulean is a highly experimental [Matrix](https://matrix.org) client intended to
4 | demonstrate the viability of freestyle public threaded conversations a la Twitter.
5 |
6 | As such, it is built as simply as possible, in order to demonstrate to someone
7 | unfamiliar with Matrix how the Client Server API can be used in this manner.
8 | It has no dependencies (other than create-react-app) and has no optimisations.
9 | It uses a naive View+Model architecture for legibility (although ideally it'd
10 | grow to be MVVM in future).
11 |
12 | For more info, see https://matrix.org/blog/2020/12/18/introducing-cerulean
13 |
14 | ## Design
15 |
16 | The way Cerulean works is:
17 | * Messages are sent into 2 rooms: the 'user timeline' room and a 'thread' room.
18 | * For instance, my user timeline room would be #@matthew:matrix.org
19 | * A thread room is created for each unique post. Replies to the thread are sent into this room.
20 | * Messages are viewed in the context of a given 'thread' room.
21 | * e.g. https://cerulean/#/@matthew:matrix.org/!cURbafjkfsMDVwdRDQ:matrix.org/$nqeHq7lJyFp4UZNlE3rN4xPVsez0vZnIcaM6SQB9waw
22 | is a given message that I've sent, and that is a permalink to the message with surrounding replies.
23 | * User timelines are viewed in the context of a given 'user timeline' room.
24 | * e.g https://cerulean/#/@matthew:matrix.org is my user timeline which has all my posts and all my replies.
25 | * Messages are threaded in 'thread' rooms using MSC2836.
26 | * Users **should** only `/join` other's timeline rooms to 'follow' them and get updates whenever they make a post/reply.
27 | * Users **should** only `/join` a thread room to reply to a post in that room, otherwise they should `/peek` to get a read-only view of the thread.
28 | * Users **should** start off as guests on their chosen homeserver, and then login if they want to post.
29 |
30 | Cerulean uses the following experimental [MSCs](https://matrix.org/docs/spec/proposals):
31 | * Threading from [MSC2836](https://github.com/matrix-org/matrix-doc/pull/2836)
32 | * `#@user:domain` user profile/timeline rooms from [MSC1769](https://github.com/matrix-org/matrix-doc/pull/1769)
33 | * peeking via `/sync` [MSC2753](https://github.com/matrix-org/matrix-doc/pull/2753) - optional
34 | * peeking over federation [MSC2444](https://github.com/matrix-org/matrix-doc/pull/2444) - optional
35 |
36 | ## Features
37 |
38 | * [x] User timelines
39 | * [x] User timelines with replies
40 | * [x] Individual messages with surrounding threaded conversation
41 | * [x] Ability to expand out threads to explore further
42 | * [x] Ability to display parent rather than child threads if the parent started on a different timeline
43 | * [x] Live updates as messages arrive (i.e. a `/sync` loop)
44 | * [ ] HTML messages
45 | * [ ] Likes
46 | * [ ] RTs
47 |
48 | Pending serverside work:
49 | * [ ] Search. We don't currently have a fast search across all public rooms, but it could of course be added.
50 | * [ ] Hashtags. These are effectively a subset of search.
51 |
52 | This test jig could also be used for experimenting with other threaded conversation formats, e.g:
53 | * Mailing lists
54 | * Newsgroups
55 | * HN/Reddit style forums
56 |
57 | ## To build
58 |
59 | ```
60 | yarn install
61 | yarn start
62 | ```
63 |
64 | ## License
65 |
66 | All files in this repository are licensed as follows:
67 |
68 | ```
69 | Copyright 2020 The Matrix.org Foundation C.I.C.
70 |
71 | Licensed under the Apache License, Version 2.0 (the "License");
72 | you may not use this file except in compliance with the License.
73 | You may obtain a copy of the License at
74 |
75 | http://www.apache.org/licenses/LICENSE-2.0
76 |
77 | Unless required by applicable law or agreed to in writing, software
78 | distributed under the License is distributed on an "AS IS" BASIS,
79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
80 | See the License for the specific language governing permissions and
81 | limitations under the License.
82 | ```
83 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | SHA=$(git rev-parse --short HEAD)
4 | npm run build
5 | tar -zcvf $SHA.tar.gz build/
6 |
--------------------------------------------------------------------------------
/etc/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80 default ipv6only=on;
4 |
5 | root /usr/share/nginx/html;
6 | index index.html;
7 |
8 | server_tokens off;
9 | server_name _;
10 |
11 | gzip on;
12 | gzip_disable "msie6";
13 |
14 | gzip_vary on;
15 | gzip_proxied any;
16 | gzip_comp_level 6;
17 | gzip_buffers 16 8k;
18 | gzip_http_version 1.1;
19 | gzip_min_length 0;
20 | gzip_types text/plain application/javascript text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;
21 |
22 | location / {
23 | try_files $uri /index.html;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/helm/cerulean/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/helm/cerulean/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: cerulean
3 | description: A Helm chart for Cerulean
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.1.0
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | # It is recommended to use it with quotes.
24 | appVersion: "1.16.0"
25 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. Get the application URL by running these commands:
2 | {{- if .Values.ingress.enabled }}
3 | {{- range $host := .Values.ingress.hosts }}
4 | {{- range .paths }}
5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
6 | {{- end }}
7 | {{- end }}
8 | {{- else if contains "NodePort" .Values.service.type }}
9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "cerulean.fullname" . }})
10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
11 | echo http://$NODE_IP:$NODE_PORT
12 | {{- else if contains "LoadBalancer" .Values.service.type }}
13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available.
14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "cerulean.fullname" . }}'
15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "cerulean.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
16 | echo http://$SERVICE_IP:{{ .Values.service.port }}
17 | {{- else if contains "ClusterIP" .Values.service.type }}
18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "cerulean.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
20 | echo "Visit http://127.0.0.1:8080 to use your application"
21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
22 | {{- end }}
23 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "cerulean.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "cerulean.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "cerulean.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "cerulean.labels" -}}
37 | helm.sh/chart: {{ include "cerulean.chart" . }}
38 | {{ include "cerulean.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "cerulean.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "cerulean.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "cerulean.serviceAccountName" -}}
57 | {{- if .Values.serviceAccount.create }}
58 | {{- default (include "cerulean.fullname" .) .Values.serviceAccount.name }}
59 | {{- else }}
60 | {{- default "default" .Values.serviceAccount.name }}
61 | {{- end }}
62 | {{- end }}
63 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "cerulean.fullname" . }}
5 | labels:
6 | {{- include "cerulean.labels" . | nindent 4 }}
7 | spec:
8 | {{- if not .Values.autoscaling.enabled }}
9 | replicas: {{ .Values.replicaCount }}
10 | {{- end }}
11 | selector:
12 | matchLabels:
13 | {{- include "cerulean.selectorLabels" . | nindent 6 }}
14 | template:
15 | metadata:
16 | {{- with .Values.podAnnotations }}
17 | annotations:
18 | {{- toYaml . | nindent 8 }}
19 | {{- end }}
20 | labels:
21 | {{- include "cerulean.selectorLabels" . | nindent 8 }}
22 | spec:
23 | {{- with .Values.imagePullSecrets }}
24 | imagePullSecrets:
25 | {{- toYaml . | nindent 8 }}
26 | {{- end }}
27 | serviceAccountName: {{ include "cerulean.serviceAccountName" . }}
28 | securityContext:
29 | {{- toYaml .Values.podSecurityContext | nindent 8 }}
30 | containers:
31 | - name: {{ .Chart.Name }}
32 | securityContext:
33 | {{- toYaml .Values.securityContext | nindent 12 }}
34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
35 | imagePullPolicy: {{ .Values.image.pullPolicy }}
36 | ports:
37 | - name: http
38 | containerPort: {{ .Values.service.port }}
39 | protocol: TCP
40 | livenessProbe:
41 | httpGet:
42 | path: /
43 | port: http
44 | readinessProbe:
45 | httpGet:
46 | path: /
47 | port: http
48 | resources:
49 | {{- toYaml .Values.resources | nindent 12 }}
50 | {{- with .Values.nodeSelector }}
51 | nodeSelector:
52 | {{- toYaml . | nindent 8 }}
53 | {{- end }}
54 | {{- with .Values.affinity }}
55 | affinity:
56 | {{- toYaml . | nindent 8 }}
57 | {{- end }}
58 | {{- with .Values.tolerations }}
59 | tolerations:
60 | {{- toYaml . | nindent 8 }}
61 | {{- end }}
62 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/hpa.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.autoscaling.enabled }}
2 | apiVersion: autoscaling/v2beta1
3 | kind: HorizontalPodAutoscaler
4 | metadata:
5 | name: {{ include "cerulean.fullname" . }}
6 | labels:
7 | {{- include "cerulean.labels" . | nindent 4 }}
8 | spec:
9 | scaleTargetRef:
10 | apiVersion: apps/v1
11 | kind: Deployment
12 | name: {{ include "cerulean.fullname" . }}
13 | minReplicas: {{ .Values.autoscaling.minReplicas }}
14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15 | metrics:
16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
17 | - type: Resource
18 | resource:
19 | name: cpu
20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
21 | {{- end }}
22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
23 | - type: Resource
24 | resource:
25 | name: memory
26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
27 | {{- end }}
28 | {{- end }}
29 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | {{- $fullName := include "cerulean.fullname" . -}}
3 | {{- $svcPort := .Values.service.port -}}
4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
7 | {{- end }}
8 | {{- end }}
9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
10 | apiVersion: networking.k8s.io/v1
11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
12 | apiVersion: networking.k8s.io/v1beta1
13 | {{- else -}}
14 | apiVersion: extensions/v1beta1
15 | {{- end }}
16 | kind: Ingress
17 | metadata:
18 | name: {{ $fullName }}
19 | labels:
20 | {{- include "cerulean.labels" . | nindent 4 }}
21 | {{- with .Values.ingress.annotations }}
22 | annotations:
23 | {{- toYaml . | nindent 4 }}
24 | {{- end }}
25 | spec:
26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
27 | ingressClassName: {{ .Values.ingress.className }}
28 | {{- end }}
29 | {{- if .Values.ingress.tls }}
30 | tls:
31 | {{- range .Values.ingress.tls }}
32 | - hosts:
33 | {{- range .hosts }}
34 | - {{ . | quote }}
35 | {{- end }}
36 | secretName: {{ .secretName }}
37 | {{- end }}
38 | {{- end }}
39 | rules:
40 | {{- range .Values.ingress.hosts }}
41 | - host: {{ .host | quote }}
42 | http:
43 | paths:
44 | {{- range .paths }}
45 | - path: {{ .path }}
46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
47 | pathType: {{ .pathType }}
48 | {{- end }}
49 | backend:
50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
51 | service:
52 | name: {{ $fullName }}
53 | port:
54 | number: {{ $svcPort }}
55 | {{- else }}
56 | serviceName: {{ $fullName }}
57 | servicePort: {{ $svcPort }}
58 | {{- end }}
59 | {{- end }}
60 | {{- end }}
61 | {{- end }}
62 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "cerulean.fullname" . }}
5 | labels:
6 | {{- include "cerulean.labels" . | nindent 4 }}
7 | spec:
8 | type: {{ .Values.service.type }}
9 | ports:
10 | - port: {{ .Values.service.port }}
11 | targetPort: http
12 | protocol: TCP
13 | name: http
14 | selector:
15 | {{- include "cerulean.selectorLabels" . | nindent 4 }}
16 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "cerulean.serviceAccountName" . }}
6 | labels:
7 | {{- include "cerulean.labels" . | nindent 4 }}
8 | {{- with .Values.serviceAccount.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | {{- end }}
13 |
--------------------------------------------------------------------------------
/helm/cerulean/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "cerulean.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "cerulean.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "cerulean.fullname" . }}:{{ .Values.service.port }}']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/helm/cerulean/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for cerulean.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 |
5 | replicaCount: 1
6 |
7 | image:
8 | repository: ghcr.io/matrix-org/cerulean
9 | pullPolicy: IfNotPresent
10 | # Overrides the image tag whose default is the chart appVersion.
11 | tag: "main"
12 |
13 | imagePullSecrets: []
14 | nameOverride: ""
15 | fullnameOverride: ""
16 |
17 | serviceAccount:
18 | # Specifies whether a service account should be created
19 | create: true
20 | # Annotations to add to the service account
21 | annotations: {}
22 | # The name of the service account to use.
23 | # If not set and create is true, a name is generated using the fullname template
24 | name: ""
25 |
26 | podAnnotations: {}
27 |
28 | podSecurityContext: {}
29 | # fsGroup: 2000
30 |
31 | securityContext: {}
32 | # capabilities:
33 | # drop:
34 | # - ALL
35 | # readOnlyRootFilesystem: true
36 | # runAsNonRoot: true
37 | # runAsUser: 1000
38 |
39 | service:
40 | type: ClusterIP
41 | port: 80
42 |
43 | ingress:
44 | enabled: false
45 | className: ""
46 | annotations: {}
47 | # kubernetes.io/ingress.class: nginx
48 | # kubernetes.io/tls-acme: "true"
49 | hosts: []
50 | #- host: chart-example.local
51 | # paths:
52 | # - path: /
53 | # pathType: ImplementationSpecific
54 | tls: []
55 | # - secretName: chart-example-tls
56 | # hosts:
57 | # - chart-example.local
58 |
59 | resources: {}
60 | # We usually recommend not to specify default resources and to leave this as a conscious
61 | # choice for the user. This also increases chances charts run on environments with little
62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
64 | # limits:
65 | # cpu: 100m
66 | # memory: 128Mi
67 | # requests:
68 | # cpu: 100m
69 | # memory: 128Mi
70 |
71 | autoscaling:
72 | enabled: false
73 | minReplicas: 1
74 | maxReplicas: 100
75 | targetCPUUtilizationPercentage: 80
76 | # targetMemoryUtilizationPercentage: 80
77 |
78 | nodeSelector: {}
79 |
80 | tolerations: []
81 |
82 | affinity: {}
83 |
--------------------------------------------------------------------------------
/helm/cr.yaml:
--------------------------------------------------------------------------------
1 | release-name-template: "helm-{{ .Name }}-{{ .Version }}"
--------------------------------------------------------------------------------
/helm/ct.yaml:
--------------------------------------------------------------------------------
1 | remote: origin
2 | target-branch: main
3 | chart-dirs:
4 | - helm
5 | validate-maintainers: false
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cerulean",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.13.1",
7 | "react-dom": "^16.13.1"
8 | },
9 | "devDependencies": {
10 | "@testing-library/jest-dom": "^4.2.4",
11 | "@testing-library/react": "^9.3.2",
12 | "@testing-library/user-event": "^7.1.2",
13 | "prettier": "^2.1.2",
14 | "react-scripts": "3.4.3"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "prettier": {
26 | "trailingComma": "es5",
27 | "tabWidth": 4,
28 | "semi": true,
29 | "singleQuote": false
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/Inter.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matrix-org/cerulean/6c9d7814f63bdc5bf62a5e68dc9fbbdf118cc338/public/Inter.ttf
--------------------------------------------------------------------------------
/public/chevron.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/close.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/delete.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matrix-org/cerulean/6c9d7814f63bdc5bf62a5e68dc9fbbdf118cc338/public/favicon.ico
--------------------------------------------------------------------------------
/public/filter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
329 | );
330 | }
331 |
332 | /**
333 | * Render a main content page depending on this.state.page
334 | * Possible options are:
335 | * - status: A permalink to a single event with replies beneath
336 | * - timeline: The aggregated feed of all users the logged in user is following.
337 | * - user: An arbitrary user's timeline. If the user is the logged in user, an input box to post a message is also displayed.
338 | */
339 | renderPage() {
340 | if (!this.props.client.accessToken) {
341 | if (this.state.recaptchaGuest) {
342 | return (
343 |
347 | );
348 | } else {
349 | return
160 | );
161 | continue;
162 | }
163 | // this array is in the order from POST /event_relationships which is
164 | // recent first
165 | const children = this.state.parentToChildren.get(eventId);
166 | if (children) {
167 | // we only render children if we aren't going to go over (hence +1) the max depth, else
168 | // we permalink to the parent with a "see more" link. Inject this first as it's a LIFO stack
169 | if (depth + 1 >= maxDepth) {
170 | toProcess.push({
171 | eventId: eventId,
172 | depth: depth + 1,
173 | seeMore: true,
174 | });
175 | } else {
176 | // render up to maxBreadth children
177 | if (children.length > maxBreadth) {
178 | // only show the first 5 then add a 'see more' link which permalinks you
179 | // to the parent which has so many children (we only display all children
180 | // on the permalink for the parent). We inject this first as it's a LIFO stack
181 | toProcess.push({
182 | eventId: eventId,
183 | depth: depth + 1,
184 | seeMore: true,
185 | });
186 | }
187 | // The array is recent first, but we want to display the most recent message at the top of the screen
188 | // so loop backwards from our cut-off to 0 (as it's LIFO we want the most recent message pushed last)
189 | for (
190 | let i = Math.min(children.length, maxBreadth) - 1;
191 | i >= 0;
192 | i--
193 | ) {
194 | //for (let i = 0; i < children.length && i < maxBreadth; i++) {
195 | toProcess.push({
196 | eventId: children[i].event_id,
197 | depth: depth + 1,
198 | });
199 | }
200 | }
201 | } else {
202 | // just because we don't have the children doesn't mean they don't exist,
203 | // check the event for children
204 | let remoteChildCount =
205 | event.unsigned?.children?.["m.reference"];
206 | if (remoteChildCount > 0) {
207 | toProcess.push({
208 | eventId: eventId,
209 | depth: depth + 1,
210 | seeMore: true,
211 | });
212 | }
213 | }
214 | rendered.push(
215 |
216 |
217 |
218 | );
219 | }
220 | return
{rendered}
;
221 | }
222 |
223 | renderVerticalChild(ev, sibling, numSiblings) {
224 | // walk the graph depth first, we want to convert graphs like:
225 | // A
226 | // / \
227 | // B C
228 | // |
229 | // D
230 | // |
231 | // E
232 | // into:
233 | // [ Message A ]
234 | // |-[ Message B ]
235 | // |-[ Message C ]
236 | // [ Message D ]
237 | // [ Message E ]
238 | // Indentation and thread lines occur on events which have siblings.
239 | const maxItems = 200;
240 | // which item to render next
241 | const toProcess = [
242 | {
243 | eventId: ev.event_id,
244 | siblingDepth: 0, // how many parents have siblings up to the root node
245 | numSiblings: numSiblings, // total number of sibling this node has (incl. itself)
246 | sibling: sibling, // the 0-based index of this sibling
247 | depthsOfParentsWhoHaveMoreSiblings: [], // depth values
248 | depth: 0, // the depth of this event
249 | },
250 | ];
251 | const rendered = [];
252 | while (toProcess.length > 0 && rendered.length < maxItems) {
253 | const procInfo = toProcess.pop();
254 | const eventId = procInfo.eventId;
255 | const siblingDepth = procInfo.siblingDepth;
256 | const numSiblings = procInfo.numSiblings;
257 | const sibling = procInfo.sibling;
258 | const isLastSibling = sibling === 0;
259 | const depth = procInfo.depth;
260 | const depthsOfParentsWhoHaveMoreSiblings =
261 | procInfo.depthsOfParentsWhoHaveMoreSiblings;
262 | // continue the thread line down to the next sibling,
263 | const msgStyle = {
264 | borderLeft: !isLastSibling ? "1px solid #8D99A5" : undefined,
265 | };
266 | const event = this.state.eventMap.get(eventId);
267 | if (!event) {
268 | continue;
269 | }
270 |
271 | // We draw tube lines going down past nested events so we need to continue
272 | // the line first before we even handle the event we're processing.
273 | let parentThreadLines = [];
274 | for (let i = 0; i <= siblingDepth; i++) {
275 | let cn = "blankThreadLine";
276 | if (depthsOfParentsWhoHaveMoreSiblings.indexOf(i) !== -1) {
277 | // add a thread line
278 | cn = "threadLine";
279 | }
280 | parentThreadLines.push();
281 | }
282 | let threadLines = (
283 |
{parentThreadLines}
284 | );
285 |
286 | if (procInfo.seeMore) {
287 | const link = createPermalinkForThreadEvent(event);
288 | let seeMoreStyle = {};
289 | // If we're "seeing more" due to capping the breadth we want the link to be left-aligned
290 | // with the thread line, else we want to indent it so it appears as a child (depth see more)
291 | if (procInfo.seeMoreDepth) {
292 | seeMoreStyle = { marginLeft: "20px" };
293 | }
294 | rendered.push(
295 |
301 | );
302 | continue;
303 | }
304 |
305 | // Copy depthsOfParentsWhoHaveMoreSiblings and add in this depth if we have more
306 | // siblings to render; this determines whether to draw outer thread lines
307 | const newDepthsOfParents = isLastSibling
308 | ? [...depthsOfParentsWhoHaveMoreSiblings]
309 | : [siblingDepth, ...depthsOfParentsWhoHaveMoreSiblings];
310 |
311 | // this array is in the order from POST /event_relationships which is
312 | // recent first
313 | const children = this.state.parentToChildren.get(eventId);
314 | if (children) {
315 | // we only render children if we aren't going to go over (hence +1) the max depth, else
316 | // we permalink to the parent with a "see more" link.
317 | if (depth + 1 >= maxDepth) {
318 | toProcess.push({
319 | eventId: eventId,
320 | siblingDepth: siblingDepth,
321 | seeMore: true,
322 | seeMoreDepth: true,
323 | numSiblings: children.length,
324 | sibling: maxBreadth,
325 | // we render the "see more" link directly underneath
326 | depthsOfParentsWhoHaveMoreSiblings: newDepthsOfParents,
327 | depth: depth + 1,
328 | });
329 | } else {
330 | const newSiblingDepth =
331 | siblingDepth + (children.length > 1 ? 1 : 0);
332 | if (children.length > maxBreadth) {
333 | // only show the first maxBreadth then add a 'see more' link which permalinks you
334 | // to the parent which has so many children (we only display all children
335 | // on the permalink for the parent). We inject this first as it's a LIFO stack
336 | toProcess.push({
337 | eventId: eventId,
338 | siblingDepth: newSiblingDepth,
339 | seeMore: true,
340 | numSiblings: children.length,
341 | sibling: maxBreadth,
342 | depthsOfParentsWhoHaveMoreSiblings: newDepthsOfParents,
343 | depth: depth + 1,
344 | });
345 | }
346 |
347 | // The array is recent first, but we want to display the most recent message at the top of the screen
348 | // so loop backwards from our cut-off to 0
349 | for (
350 | let i = Math.min(children.length, maxBreadth) - 1;
351 | i >= 0;
352 | i--
353 | ) {
354 | //for (let i = 0; i < children.length && i < maxBreadth; i++) {
355 | toProcess.push({
356 | eventId: children[i].event_id,
357 | siblingDepth: newSiblingDepth,
358 | numSiblings: children.length,
359 | // rendering relies on a stack so invert the sibling order, pretending the middle of the array is sibling 0
360 | sibling:
361 | Math.min(children.length, maxBreadth) - 1 - i,
362 | parentIsLastSibling: isLastSibling,
363 | depthsOfParentsWhoHaveMoreSiblings: newDepthsOfParents,
364 | depth: depth + 1,
365 | });
366 | }
367 | }
368 | } else {
369 | // just because we don't have the children doesn't mean they don't exist,
370 | // check the event for children
371 | let remoteChildCount =
372 | event.unsigned?.children?.["m.reference"];
373 | if (remoteChildCount > 0) {
374 | toProcess.push({
375 | eventId: eventId,
376 | siblingDepth: siblingDepth,
377 | seeMore: true,
378 | seeMoreDepth: true,
379 | numSiblings: remoteChildCount,
380 | sibling: maxBreadth,
381 | // we render the "see more" link directly underneath
382 | depthsOfParentsWhoHaveMoreSiblings: newDepthsOfParents,
383 | depth: depth + 1,
384 | });
385 | }
386 | }
387 |
388 | // if there's multiple siblings then they all get corners to fork off from the parent
389 | // if there's only 1 sibling then we just put the reply directly beneath without a corner
390 | let threadCorner;
391 | if (numSiblings > 1) {
392 | let threadCornerType = "/thread-line.svg";
393 | let threadCornerClass = "threadFork";
394 | if (isLastSibling) {
395 | threadCornerType = "/thread-corner.svg";
396 | threadCornerClass = "threadCorner";
397 | }
398 | threadCorner = (
399 |
404 | );
405 | }
406 |
407 | rendered.push(
408 |
146 | {this.state.timeline
147 | .filter((ev) => {
148 | // only messages sent by this user
149 | if (
150 | ev.type !== "m.room.message" ||
151 | ev.sender !== this.props.userId
152 | ) {
153 | return false;
154 | }
155 | // only messages with cerulean fields
156 | if (
157 | !ev.content["org.matrix.cerulean.event_id"]
158 | ) {
159 | return false;
160 | }
161 | // all posts and replies
162 | if (this.state.withReplies) {
163 | return true;
164 | }
165 | // only posts
166 | if (ev.content["org.matrix.cerulean.root"]) {
167 | return true;
168 | }
169 | return false;
170 | })
171 | .map((ev) => {
172 | hasEntries = true;
173 | return (
174 |
180 | );
181 | })}
182 |
183 | );
184 | if (!hasEntries) {
185 | // the default page is / which is TimelinePage which then directs them to
186 | // their UserPage if there are no events, so we want to suggest some content
187 | let emptyListText;
188 | if (this.state.isMe) {
189 | emptyListText = (
190 |
191 | No posts yet. Check the{" "}
192 |
197 | welcome post
198 |
199 | .
200 |
201 | );
202 | } else {
203 | emptyListText = (
204 | This user hasn't posted anything yet.
205 | );
206 | }
207 |
208 | timelineBlock = (
209 |