Compare commits

..

157 Commits

Author SHA1 Message Date
Ariadne Conill dd3a2ce506 Merge gitea.treehouse.systems:mirrors/mastodon-glitch into chore/merge-20230511-1
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-05-11 17:27:17 -07:00
Claire f08f6d20e6
Merge pull request #2215 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2023-05-09 14:47:11 +02:00
Renaud Chaput ea04f33f50 [Glitch] Fixes build errors from some previous TS-related commits
Port 45579a26cf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-08 22:15:00 +02:00
fusagiko / takayamaki 3d9e35375c [Glitch] Rewrite actions/app.ts and reducers/missed_updates.ts with createAction
Port 0999cb4601 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-08 22:15:00 +02:00
Renaud Chaput a33d6c946a [Glitch] Remove MastodonMap TS type
Port 9a52a7f7a0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2023-05-08 22:15:00 +02:00
kouhai dev acf635ffe8 th: cache busting? not on our watch
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-04-24 22:09:57 -07:00
kouhai dev fdd48f3b43 th: date-based tags suck, actually 2023-04-24 22:09:53 -07:00
kouhai dev 74742ba16d th: add woodpecker config to dockerignore 2023-04-24 22:09:47 -07:00
kouhai dev f177325ba7 th: better image tags hopefully 2023-04-24 22:09:44 -07:00
kouhai dev e7ef14a4d7 Merge remote-tracking branch 'glitch/main' 2023-04-23 23:23:28 -07:00
kouhai dev 7e1924e3fb Merge remote-tracking branch 'glitch/main'
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-04-22 01:48:47 -07:00
kouhai dev d5e19ad44a th: everyone (doesn't) gets rspec'd in ci :3
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-04-22 01:21:55 -07:00
kouhai dev 2a31266b9d th: speedy clone for fast(er) ci
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-04-22 00:38:08 -07:00
kouhai dev ad9c9bb74b Merge remote-tracking branch 'glitch/main' 2023-04-22 00:38:08 -07:00
kouhai dev 6b29db907c th: fix presenter before merge 2023-04-22 00:38:08 -07:00
kouhai dev c13cde3589 th: fix build 2023-04-22 00:38:08 -07:00
kouhai dev 7499f170b8 th: fix extension 2023-04-22 00:38:08 -07:00
kouhai dev 096d584d9a th: hack: fix excessively noisy babel traces 2023-04-22 00:38:08 -07:00
kouhai dev d8b8b3d3fc th: ugh 2023-04-22 00:38:08 -07:00
kouhai dev b822d60675 th: compose out of the box, more-or-less 2023-04-22 00:38:08 -07:00
kouhai dev c9a49fc3dd th: fun, image tagging, and SOURCE_TAG 2023-04-22 00:38:08 -07:00
kouhai dev 0382d1efe7 th: optimize dockerfile (attempt was made) 2023-04-22 00:38:08 -07:00
kouhai dev 236f063dd3 th: berry flavored yarn 2023-04-22 00:38:08 -07:00
kouhai dev a2faa867a5 th: update js deps 2023-04-22 00:38:08 -07:00
kouhai dev 662a83326d Merge remote-tracking branch 'glitch/main' 2023-04-22 00:38:08 -07:00
kouhai dev 85799852d4 th: Add foreman to development deps 2023-04-22 00:38:08 -07:00
kouhai dev 66ed2c4288 th: use {RAILS,NODE}_ENV=development when running webpack-dev-server with Foreman 2023-04-22 00:38:08 -07:00
kouhai dev 01c7d03aeb Merge remote-tracking branch 'glitch/main' 2023-04-22 00:38:08 -07:00
kouhai dev 12906c7d54 th: update models, node/Gemfile state 2023-04-22 00:38:08 -07:00
kouhai dev 351f0cf51a Merge remote-tracking branch 'glitch/main' 2023-04-22 00:38:08 -07:00
kouhai dev f6c88fb2a6 Merge remote-tracking branch 'glitch/main' 2023-04-22 00:38:08 -07:00
kouhai dev 6a0b004e7b th: consistent dividers 2023-04-22 00:38:08 -07:00
kouhai dev bae4217549 th: add deps rake tasks 2023-04-22 00:38:08 -07:00
kouhai dev 8a10baa676 update default repository/source metadata 2022-12-29 22:55:49 -06:00
kouhai dev 051f883fbf update browserslist to remove unnecessary "not IE 11" 2022-12-29 22:55:49 -06:00
kouhai dev c762da600a update dockerignore 2022-12-29 22:38:44 -06:00
kouhai dev dec67bc50e temporarily remove "Get the app" link" 2022-12-29 22:38:44 -06:00
kouhai dev 0bd08b1169 quick status hover hack 2022-12-29 22:38:43 -06:00
kouhai dev 0ff80581b9 fix lints
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-29 22:23:09 -06:00
kouhai dev 4424dbad4b fix missing qt summary autofill 2022-12-29 22:23:04 -06:00
Ariadne Conill 31550094b3 Merge pull request 'merge in mastodon 4.0.2+glitch' (#41) from merge/glitch-4.0.2 into main
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: #41
2022-12-28 22:38:23 +00:00
Ariadne Conill ba8847da0e woodpecker: use latest docker
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-12-28 22:27:57 +00:00
Ariadne Conill c66041370c Merge https://gitea.treehouse.systems/mirrors/mastodon-glitch into merge/glitch-4.0.2
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-12-28 22:08:51 +00:00
Ariadne Conill 98d9c84c8b Merge pull request 'Turn quote author/text into links back to the original profile/post' (#37) from ar/mastodon:turn-quotes-into-links into main
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: #37
2022-12-28 06:59:46 +00:00
Ariadne Conill 5d0ed01191 Merge branch 'main' into turn-quotes-into-links
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-12-28 06:59:34 +00:00
Ariadne Conill 9c51dc8dd5 activitypub: tag manager: ensure that a quote post has the OP included in to field
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-28 06:48:24 +00:00
Ariadne Conill 418fffcb41 models: status edit: pass through quote information to parent status 2022-12-28 06:06:49 +00:00
Ariadne Conill 985ef99590 activitypub: create: fix up quoteUri/quoteUrl/quoteURL handling
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-28 05:15:25 +00:00
Robert Gerus 5025713c5a status: turn quote author/text into links to original profile/post
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-12-28 01:35:56 +01:00
Ariadne Conill 384ac613d8 reducers: add missing nullification of quote_id on reset
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-26 05:54:44 +00:00
Ariadne Conill c6a4f42a37 compose reducer: fix cancelling reply/quote
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-26 05:39:30 +00:00
Ariadne Conill 8b6e2ed562 Merge pull request 'add quote toots' (#36) from feature/quote into main
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: #36
2022-12-26 04:52:20 +00:00
Ariadne Conill c4253c32a0 delete obsolete console.log statements
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-12-26 04:38:32 +00:00
Ariadne Conill df07456f51 add reply and quote icons to the reply/quote indicators so people know what is going on 2022-12-26 04:38:21 +00:00
Ariadne Conill 7efe4bc5d3 activitypub: fix context extensions for quote_uri 2022-12-26 04:26:52 +00:00
Ariadne Conill f0065720d6 javascript: fix dispatch 2022-12-26 04:17:21 +00:00
Ariadne Conill d23cd8da00 add quote option to detailed statuses 2022-12-26 04:09:05 +00:00
Ariadne Conill 67a7b6067a components: detailed status: suppress cards on quote posts 2022-12-26 03:59:38 +00:00
Ariadne Conill 7d4127065d formatting helper: add the quote-inline hack for incompatible clients 2022-12-26 03:57:02 +00:00
Ariadne Conill e59c40eb68 activitypub: switch to fedibird:quoteUri 2022-12-26 03:18:03 +00:00
Ariadne Conill 5a8d4265ef glitch: fix up quote indicator 2022-12-26 02:53:01 +00:00
Ariadne Conill 214a4c9e6b glitch: reducers: set up correct state for quoting 2022-12-26 01:41:45 +00:00
Ariadne Conill 766a643811 add styles for quote indicator 2022-12-26 01:41:32 +00:00
Ariadne Conill a47d917072 flavors: glitch: add quote handling to status feature 2022-12-26 01:31:57 +00:00
Ariadne Conill c7e00d4c4e flavors: glitch: add quote indicator component 2022-12-26 01:27:58 +00:00
Ariadne Conill 9d4851e3cd glitch: actions: add quoteCompose and cancelQuoteCompose 2022-12-26 01:23:25 +00:00
Ariadne Conill adf1e9fc2e flavors: glitch: action bar: add quote button 2022-12-26 01:15:54 +00:00
Ariadne Conill 08aecd24ba flavors: glitch: show emojified display name in quotes 2022-12-26 00:59:04 +00:00
Ariadne Conill 005256ae8c javascript: glitch: start rendering quotes 2022-12-25 21:11:11 +00:00
Ariadne Conill 5be6a59f80 javascript: glitch: dont render cards if the status has a quote attached 2022-12-25 10:58:25 +00:00
Ariadne Conill 0d3df3e8cf javascript: glitch: pre-process misskey quotes to remove the URL part 2022-12-25 10:46:18 +00:00
Ariadne Conill b36e884cc1 activitypub: note serializer: support _misskey keys 2022-12-25 09:16:44 +00:00
Ariadne Conill 14d001574c activitypub: case transform: support _misskey keys without messing them up 2022-12-25 09:16:44 +00:00
Ariadne Conill 61565488a6 status: support either _misskey_quote or quoteUrl for fetching quotes 2022-12-25 09:16:42 +00:00
Ariadne Conill 8d86c77a58 db: add quote_id index 2022-12-25 04:20:17 +00:00
Ariadne Conill 36955a7a56 status: prevent recursion when serializing 2022-12-25 04:20:17 +00:00
Ariadne Conill 1cef1eb847 status: disallow quoting of non-public posts 2022-12-25 04:20:16 +00:00
Ariadne Conill a697e1da13 db: add quote_id migration 2022-12-25 03:58:18 +00:00
Ariadne Conill 0b48ae2c3c sanitizer config: add quote-inline span to allowlist 2022-12-25 03:58:18 +00:00
Ariadne Conill 1df2577b89 db: add quote_id to statuses table 2022-12-25 03:58:18 +00:00
Ariadne Conill 28fb5c8c52 rest: status serializer: include quote data 2022-12-25 03:58:18 +00:00
Ariadne Conill 56d4b04358 views: add quote status html view 2022-12-25 03:58:16 +00:00
Ariadne Conill ee98c0a6f8 services: post status service: add quote_id to status parameters 2022-12-25 03:25:22 +00:00
Ariadne Conill ba965bec3d statuses controller: accept quote_id parameter 2022-12-25 03:25:22 +00:00
Ariadne Conill 968bd6f0ee activitypub: resolve quoted objects when new create activities are received 2022-12-25 03:25:22 +00:00
Ariadne Conill 6b07407820 context helper: add quoteUrl as as:quoteUrl, even though its wrong 2022-12-25 03:25:22 +00:00
Ariadne Conill 0990d5ac75 activitypub: note serializer: begrudgingly serialize quotes using misskey quoteUrl 2022-12-25 03:25:22 +00:00
Ariadne Conill b1bce9d193 models: status: add support for quoting 2022-12-25 03:25:20 +00:00
Ariadne Conill 78e8693388 services: link fetcher: do not fetch links for quotes 2022-12-25 02:00:29 +00:00
Ariadne Conill 6be7cbb0bc Merge pull request 'docker-compose: emulate production traefik setup' (#35) from feature/docker-compose-traefik into main
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: #35
2022-12-25 01:57:01 +00:00
Ariadne Conill 4bb0e9f9ed docker-compose: emulate production traefik setup
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-12-25 01:56:20 +00:00
fox 772ba5aac3 update documentation
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: #30
Co-authored-by: fox <fox@neko.business>
Co-committed-by: fox <fox@neko.business>
2022-12-18 03:13:45 +00:00
Rin aa24b2d072
make glitch style consistent
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-08 18:26:32 +11:00
Rin 0cd76aa22a
make default masto style consistent 2022-12-08 18:24:41 +11:00
Rin 06a2259577
fix default masto style too 2022-12-08 16:58:48 +11:00
Rin 7bf26a1094
fix missing link style in admin.scss - actually this time
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-12-08 16:31:42 +11:00
kouhai dev 41f55e5d8f update upstream glitch README/CONTRIBUTING
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-27 23:49:18 -06:00
kouhai dev c75ac19c8a fix REDIS_URL unix socket path parsing for relative paths 2022-11-27 23:49:18 -06:00
kouhai dev bd0da3a499 fix redis conf
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-24 23:01:20 -06:00
Rin e60d7e3c43
Add dir for redis data
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-11-25 14:24:44 +11:00
kouhai dev ea44d46ca9 update gitignore
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-24 18:58:23 -06:00
kouhai dev 17ba99b157 rewrite instructions for a self-contained dev env
ci/woodpecker/pr/woodpecker Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-24 18:57:24 -06:00
Rin f4b48ca5ee
Command style consistency
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-23 19:35:13 +11:00
Rin 8e4da8677c
Fix headings
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
2022-11-23 19:34:20 +11:00
Rin 401a51b67b
Update instructions - make everything self contained
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-23 19:31:19 +11:00
Rin 842498649d
Moderately curse the env file to contain everything in the project 2022-11-23 19:25:17 +11:00
Rin 17e49cf098
rename env file to conform 2022-11-23 19:22:32 +11:00
Rin ca98b0da84
Add .env.ENV.local to .gitignore 2022-11-23 19:10:31 +11:00
Rin 8a701c5ac3 Fix numbering <_<
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-23 07:25:24 +00:00
Rin 2790045c7e Reduce .env file to minimal config 2022-11-23 07:25:24 +00:00
Rin 74c54af904 update gitignore to ignore the pg db we created 2022-11-23 07:25:24 +00:00
Rin 59b831de9b use local pg cluster instead 2022-11-23 07:25:24 +00:00
Rin 4172c91de0 fix typo 2022-11-23 07:25:24 +00:00
Rin ced99149d6 Add SETUP instructions 2022-11-23 07:25:24 +00:00
Rin d25d8d2c07
fix no-style links in admin panel
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-21 18:34:21 +11:00
kouhai dev b247829276 Merge remote-tracking branch 'glitch/main'
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-15 12:03:46 -08:00
Ariadne Conill bb4ed39673 woodpecker: only do push when branch is main
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-13 03:34:08 +00:00
Ariadne Conill c4b8dfae32 Merge pull request 'logo-fix' (#12) from logo-fix into main
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: #12
2022-11-13 03:18:16 +00:00
Rin 4a18d8e642 Fix non-rendering on certain engines
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-13 02:12:17 +00:00
Rin c048f832c1 Change Safari bookmark colour to match treehouse 2022-11-13 02:12:17 +00:00
Rin bd6a63ebed Add small icon assets 2022-11-13 02:12:17 +00:00
Ariadne Conill b0dc619c56 woodpecker: add registry secret
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-12 14:08:48 +00:00
Ariadne Conill d51636a24d woodpecker: remove separate login step
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-11-12 11:18:48 +00:00
Ariadne Conill 5650b7a785 woodpecker: expose host docker socket for now until i have time to make docker-in-docker work
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-11-12 10:55:53 +00:00
Ariadne Conill 1678011556 add woodpecker CI
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-11-12 10:50:24 +00:00
Ariadne Conill 946388f02e Merge gitea.treehouse.systems:mirrors/mastodon-glitch 2022-11-10 07:11:03 +00:00
Ariadne Conill c515158c09 Revert "Add prefers-color-scheme based theme"
This reverts commit 652882a81b.
2022-11-10 07:05:56 +00:00
Ariadne Conill 8d1fe49d81 try to fix compile 2022-11-10 06:41:02 +00:00
Ariadne Conill ce30dbc2f9 Merge pull request 'Add prefers-color-scheme based theme' (#6) from hamptonmoore/mastodon:system-theme into main
Reviewed-on: #6
2022-11-10 06:10:04 +00:00
Ariadne Conill b1955b69eb Merge pull request 'Replace default mastodon icons with treehouse icons' (#5) from favicon-fix into main
Reviewed-on: #5
2022-11-10 06:06:10 +00:00
Hampton Moore 652882a81b Add prefers-color-scheme based theme 2022-11-09 14:33:12 -05:00
Rin da2445d301
Update favicons to use proper rounded-rect 2022-11-09 19:12:14 +11:00
Rin c4785a1e2e
Update favicons to treehouse favicons 2022-11-09 19:11:36 +11:00
Rin e5e061cdfc restore treehouse favicon 2022-11-09 17:24:22 +11:00
Ariadne Conill 1e00edfc35 config: production: traefik does not support sendfile mode 2022-11-08 09:59:32 +00:00
Ariadne Conill 8d07052d6d version: change to +glitch+th for client compatibility 2022-11-08 07:16:56 +00:00
Ariadne Conill 2c4738f592 version: change +glitch suffix to +th, since we have local modifications 2022-11-07 23:24:11 +00:00
Ariadne Conill 2bee8024c2 Merge https://gitea.treehouse.systems/treehouse/mastodon-glitch into rebase/4.0.0rc2 2022-11-07 23:20:04 +00:00
Ariadne Conill 10c928a72e fix autocomplete 2022-11-07 17:32:46 +00:00
Ariadne Conill d0e2d7df37 config: CSP: add unsafe-eval for scripts 2022-11-07 17:30:16 +00:00
Ariadne Conill 152f5c7983 app: api: base_controller: allow API access to be configurable 2022-11-06 19:13:50 +00:00
Ariadne Conill 7ceda772e4 locales: fix YAML quoting issue
Psych::SyntaxError: (/opt/mastodon/config/locales/simple_form.en.yml): mapping values are not allowed in this context at line 102 column 103
2022-11-06 18:43:54 +00:00
Ariadne Conill 8fb8a2667a Merge branch 'rebase/4.0.0rc1' 2022-11-06 18:23:41 +00:00
kouhai dev 6b7b80a892 update account request question prompt 2022-11-06 18:22:00 +00:00
Ariadne Conill a82113dbd9 vanilla: emoji: use 63x63 grid for emoji 2022-11-06 18:21:57 +00:00
Ariadne Conill 728ed66f8a glitch: emoji: set sprite sheet to 63x63 2022-11-06 18:21:52 +00:00
Ariadne Conill 117d2ed9c7 dockerfile: implement emoji-mart patch 2022-11-06 18:20:15 +00:00
Ariadne Conill 4c38a2d32c update emoji-mart sprite sheet for emoji 13.1 2022-11-06 18:20:15 +00:00
Ariadne Conill c01750a3dd add emoji 13.1 data for emoji-mart 2022-11-06 18:20:15 +00:00
kouhai dev 0ae6ca72de update account request question prompt 2022-11-06 01:36:06 -07:00
Ariadne Conill 47493323fa fixup! 2022-06-12 01:07:56 +00:00
Ariadne Conill 5f634378a2 glitch: fixup! 2022-06-12 01:07:52 +00:00
Ariadne Conill 2298d5b3ce vanilla: emoji: use 63x63 grid for emoji 2022-06-12 00:45:41 +00:00
Ariadne Conill 2ccc011e3f glitch: emoji: set sprite sheet to 63x63 2022-06-12 00:43:28 +00:00
Ariadne Conill 39f42334c2 dockerfile: implement emoji-mart patch 2022-06-11 23:57:03 +00:00
Ariadne Conill 648cad5619 update emoji-mart sprite sheet for emoji 13.1 2022-06-11 23:52:40 +00:00
Ariadne Conill a0479b5d4c add emoji 13.1 data for emoji-mart 2022-06-11 23:51:31 +00:00
105 changed files with 2270 additions and 212 deletions

View File

@ -1,6 +1,5 @@
[production] [production]
defaults defaults
not IE 11
not dead not dead
[development] [development]

View File

@ -1,21 +1,38 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Order-independent
*.sw*
*~
.DS_Store
.bundle .bundle
.env .env
.env.* .env.*
.git .git
.gitattributes .gitattributes
.gitignore
.github .github
public/system .gitignore
public/assets .woodpecker.yml
public/packs build
node_modules chart
coverage
data
elasticsearch
log
neo4j neo4j
vendor/bundle node_modules
.DS_Store
*.swp
*~
postgres postgres
postgres14 postgres14
public/assets
public/packs
public/packs-test
public/system
redis redis
elasticsearch sorbet
chart tmp
vendor/bundle

7
.env.development Normal file
View File

@ -0,0 +1,7 @@
LOCAL_DOMAIN=localhost
ALTERNATE_DOMAINS=mastodon.internal
DB_HOST=$(pwd)/data/postgres
DB_USER=mastodon
DB_NAME=mastodon_dev
REDIS_URL=unix://./data/redis/redis-dev.sock

View File

@ -14,7 +14,7 @@
# ---------- # ----------
# This identifies your server and cannot be changed safely later # This identifies your server and cannot be changed safely later
# ---------- # ----------
LOCAL_DOMAIN=example.com LOCAL_DOMAIN=localhost
# Use this only if you need to run mastodon on a different domain than the one used for federation. # Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain # You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain
@ -25,6 +25,7 @@ LOCAL_DOMAIN=example.com
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not # handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values # be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com # ALTERNATE_DOMAINS=example1.com,example2.com
ALTERNATE_DOMAINS=mastodon.internal
# Use HTTP proxy for outgoing request (optional) # Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118 # http_proxy=http://gateway.local:8118
@ -43,14 +44,14 @@ LOCAL_DOMAIN=example.com
# Redis # Redis
# ----- # -----
REDIS_HOST=localhost REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
# PostgreSQL # PostgreSQL
# ---------- # ----------
DB_HOST=/var/run/postgresql DB_HOST=db
DB_USER=mastodon DB_USER=postgres
DB_NAME=mastodon_production DB_NAME=mastodon_production
DB_PASS= DB_PASS=
DB_PORT=5432 DB_PORT=5432

View File

@ -3,3 +3,8 @@ NODE_ENV=tests
# Federation # Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true LOCAL_HTTPS=true
DB_HOST=$(pwd)/data/postgres
DB_USER=mastodon
DB_NAME=mastodon_dev
REDIS_URL=unix://./data/redis/redis-dev.sock

15
.gitignore vendored
View File

@ -4,6 +4,9 @@
# or operating system, you probably want to add a global ignore instead: # or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global' # git config --global core.excludesfile '~/.gitignore_global'
# Ignore local dotenv overrides
.env.*.local
# Ignore bundler config and downloaded libraries. # Ignore bundler config and downloaded libraries.
/.bundle /.bundle
/vendor/bundle /vendor/bundle
@ -12,6 +15,9 @@
/db/*.sqlite3 /db/*.sqlite3
/db/*.sqlite3-journal /db/*.sqlite3-journal
# Ignore local data directory
/data
# Ignore all logfiles and tempfiles. # Ignore all logfiles and tempfiles.
.eslintcache .eslintcache
/log/* /log/*
@ -63,3 +69,12 @@ yarn-debug.log
# Ignore Docker option files # Ignore Docker option files
docker-compose.override.yml docker-compose.override.yml
# Yarn Berry
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

66
.woodpecker.yml Normal file
View File

@ -0,0 +1,66 @@
variables:
environment: &docker-environment
NAME: gitea.treehouse.systems/treehouse/mastodon
DATE_COMMAND: export COMMIT_DATE=$(date -u -Idate -d @$(git show -s --format=%ct))
docker-step: &docker-step
image: docker:rc-git
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
<<: *docker-environment
clone:
git:
image: woodpeckerci/plugin-git
settings:
partial: false
depth: 10
pipeline:
build-base:
<<: *docker-step
commands:
- docker version
- docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . --target build-base -t $NAME:build-base
# the world is not yet ready for this step
# test:
# <<: *docker-step
# commands:
# - docker run --rm -e RAILS_ENV=test -e NODE_ENV=development $NAME:build-base sh -c 'bundle config set --local without development && bundle install && rake spec'
build:
<<: *docker-step
commands:
- eval $DATE_COMMAND
- export TAG=$${COMMIT_DATE}.$CI_COMMIT_SHA && echo $${TAG}
- docker image build -f Dockerfile --build-arg SOURCE_TAG=$CI_COMMIT_SHA . -t $NAME:latest
- docker tag $NAME:latest $NAME:$TAG
# idk what's actually persisted between steps
# /shrug this works, so,???
- echo $${TAG} > tags.txt
- echo latest >> tags.txt
# maybe we can use tags someday,,,
# tag-tag:
# image: *docker-git
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
# commands:
# - docker tag $NAME:latest $NAME:$CI_COMMIT_TAG
# when:
# event: tag
push:
<<: *docker-step
commands:
- echo $REGISTRY_SECRET | docker login -u $REGISTRY_USER --password-stdin gitea.treehouse.systems
- cat tags.txt | xargs -n 1 -I% echo docker image push $NAME:%
- cat tags.txt | xargs -n 1 -I% docker image push $NAME:%
when:
event: [push, tag]
branch: main
secrets: [REGISTRY_SECRET]
environment:
<<: *docker-environment
REGISTRY_USER: ariadne

873
.yarn/releases/yarn-3.4.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

5
.yarnrc.yml Normal file
View File

@ -0,0 +1,5 @@
enableGlobalCache: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.4.1.cjs

View File

@ -1,10 +1,10 @@
# Contributing to Mastodon Glitch Edition # Contributing to Mastodon Glitch+Treehouse Edition #
Thank you for your interest in contributing to the `glitch-soc` project! Thank you for your interest in contributing to the **Treehouse Mastodon** project!
Here are some guidelines, and ways you can help. Here are some guidelines, and ways you can help.
> (This document is a bit of a work-in-progress, so please bear with us. > (This document is a bit of a work-in-progress, so please bear with us.
> If you don't see what you're looking for here, please don't hesitate to reach out!) > If you don't see what you're looking for here, please don't hesitate to reach out!)
## Translations ## Translations
@ -12,26 +12,31 @@ You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.co
[![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc) [![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc)
## Planning ## Planning ##
Right now a lot of the planning for this project takes place in the `#fediverse`
channel of the Treehouse Discord, or through Gitea Issues.
Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects.
We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler. We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler.
## Documentation ## Documentation ##
The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)). The upstream Glitch documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)).
Right now, we've mostly focused on the features that make this fork different from upstream in some manner.
Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code.
## Frontend Development ## Setup ##
For a some-batteries-required guide to setting up a development environment for this repository, read Rin's excellent
[SETUP.md](https://gitea.treehouse.systems/treehouse/mastodon/src/branch/main/SETUP.md).
## Frontend Development ##
Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information. Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information.
## Backend Development ## Backend Development ##
See the guidelines below. See the guidelines below.
--- - - -
You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below. You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below.
@ -80,6 +85,8 @@ It is not always possible to phrase every change in such a manner, but it is des
- Code style rules (rubocop, eslint) - Code style rules (rubocop, eslint)
- Normalization of locale files (i18n-tasks) - Normalization of locale files (i18n-tasks)
**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run.
## Documentation ## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).

View File

@ -1,23 +1,24 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim # This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
ARG NODE_VERSION="16.20-bullseye-slim" ARG NODE_VERSION="18.15-bullseye-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.1-slim as ruby
FROM node:${NODE_VERSION} as build FROM node:${NODE_VERSION} as build-base
COPY --link --from=ruby /opt/ruby /opt/ruby COPY --link --from=ruby /opt/ruby /opt/ruby
ENV DEBIAN_FRONTEND="noninteractive" \ ENV DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin" PATH="${PATH}:/opt/ruby/bin" \
NODE_OPTIONS=--openssl-legacy-provider
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
COPY Gemfile* package.json yarn.lock /opt/mastodon/
# hadolint ignore=DL3008 # hadolint ignore=DL3008
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \ apt-get install -y --no-install-recommends build-essential \
ca-certificates \
git \ git \
libicu-dev \ libicu-dev \
libidn11-dev \ libidn11-dev \
@ -31,7 +32,12 @@ RUN apt-get update && \
ca-certificates \ ca-certificates \
libreadline8 \ libreadline8 \
python3 \ python3 \
shared-mime-info && \ shared-mime-info
COPY --link .yarn/releases/ /opt/mastodon/.yarn/releases/
COPY --link Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
RUN \
bundle config set --local deployment 'true' && \ bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \ bundle config set --local without 'development test' && \
bundle config set silence_root_warning true && \ bundle config set silence_root_warning true && \
@ -39,6 +45,23 @@ RUN apt-get update && \
yarn install --pure-lockfile --production --network-timeout 600000 && \ yarn install --pure-lockfile --production --network-timeout 600000 && \
yarn cache clean yarn cache clean
# Precompile assets
# TODO(kouhai): we're currently patching node_modules because of emoji-mart.
# we should integrate our own fork instead.
COPY --link . /opt/mastodon
FROM build-base AS build
ENV RAILS_ENV="production" \
NODE_ENV="production"
ENV OTP_SECRET=precompile_placeholder \
SECRET_KEY_BASE=precompile_placeholder \
RAKE_NO_YARN_INSTALL_HACK=1
RUN mv ./emoji_data/all.json ./node_modules/emoji-mart/data/all.json && \
bundle exec rails assets:precompile
FROM node:${NODE_VERSION} FROM node:${NODE_VERSION}
# Use those args to specify your own version flags & suffixes # Use those args to specify your own version flags & suffixes
@ -82,13 +105,14 @@ RUN apt-get update && \
# Note: no, cleaning here since Debian does this automatically # Note: no, cleaning here since Debian does this automatically
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem # See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem
COPY --chown=mastodon:mastodon . /opt/mastodon COPY --link --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
ARG SOURCE_TAG=''
ENV RAILS_ENV="production" \ ENV RAILS_ENV="production" \
NODE_ENV="production" \ NODE_ENV="production" \
RAILS_SERVE_STATIC_FILES="true" \ RAILS_SERVE_STATIC_FILES="true" \
BIND="0.0.0.0" \ BIND="0.0.0.0" \
SOURCE_TAG="${SOURCE_TAG}" \
MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \ MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}" MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"
@ -96,9 +120,6 @@ ENV RAILS_ENV="production" \
USER mastodon USER mastodon
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
# Precompile assets
RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile
# Set the work dir and the container entry point # Set the work dir and the container entry point
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 3000 4000 EXPOSE 3000 4000

View File

@ -147,6 +147,8 @@ group :development do
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
gem 'stackprof' gem 'stackprof'
gem 'foreman'
end end
group :production do group :production do

View File

@ -286,6 +286,7 @@ GEM
fog-core (>= 1.45, <= 2.1.0) fog-core (>= 1.45, <= 2.1.0)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
foreman (0.87.2)
formatador (0.3.0) formatador (0.3.0)
fugit (1.8.1) fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
@ -805,6 +806,7 @@ DEPENDENCIES
fastimage fastimage
fog-core (<= 2.4.0) fog-core (<= 2.4.0)
fog-openstack (~> 0.3) fog-openstack (~> 0.3)
foreman
fuubar (~> 2.5) fuubar (~> 2.5)
haml-rails (~> 2.0) haml-rails (~> 2.0)
haml_lint haml_lint
@ -897,3 +899,9 @@ DEPENDENCIES
webpacker (~> 5.4) webpacker (~> 5.4)
webpush! webpush!
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION
ruby 3.0.5p211
BUNDLED WITH
2.4.6

View File

@ -1,4 +1,4 @@
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
stream: env PORT=4000 yarn run start stream: env PORT=4000 yarn run start
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 webpack: env RAILS_ENV=development NODE_ENV=development ./bin/webpack-dev-server --listen-host 0.0.0.0

View File

@ -1,14 +1,20 @@
# Mastodon Glitch Edition # Mastodon Glitch+Treehouse Edition #
> Now with automated deploys! > Now with bunny ears!
[![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci] So here's the deal: we all work on this code, and anyone who uses that does so absolutely at their own risk. Can you dig it?
[![Code Climate](https://img.shields.io/codeclimate/maintainability/glitch-soc/mastodon.svg)][code_climate]
[circleci]: https://circleci.com/gh/glitch-soc/mastodon Specifically, this fork-of-a-fork is intended for Treehouse use only. Unless
[code_climate]: https://codeclimate.com/github/glitch-soc/mastodon otherwise communicated, we will not put effort into supporting other deployments
or upstreaming our patches.
So here's the deal: we all work on this code, and anyone who uses that does so absolutely at their own risk. can you dig it? ## Links
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/). - You can view upstream Glitch's documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/). - Contributing guidelines are available [here](CONTRIBUTING.md).
## Known Deployments
- Treehouse Social: [social.treehouse.systems](https://social.treehouse.systems)
- VT Social: [vt.social](https://vt.social)
- Unstable Systems: [unstable.systems](https://unstable.systems)

View File

@ -4,3 +4,12 @@
require File.expand_path('../config/application', __FILE__) require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks Rails.application.load_tasks
# please don't do this
if Rake::Task.task_defined?('assets:precompile') && ENV.include?('RAKE_NO_YARN_INSTALL_HACK')
task = Rake::Task['assets:precompile']
puts task.prerequisites
task.prerequisites.delete('webpacker:yarn_install')
task.prerequisites.delete('yarn:install')
puts task.prerequisites
end

143
SETUP.md Normal file
View File

@ -0,0 +1,143 @@
# Setting up a dev environment
## Prerequisites
Mastodon development requires the following:
- Ruby 3.0
- Ruby gems:
- `bundler`
- `irb`
- `foreman`
- NodeJS v18 (LTS)
- NPM packages:
- `yarn`
- Postgres
- Redis
### macOS
First, make sure you have Homebrew installed. Follow the instructions at [brew.sh](https://brew.sh).
Run the following to install all necessary packages:
```
brew install ruby@3.0 foreman node yarn postgresql redis
```
Ruby 3.0 is **keg-only** by default. Follow the instructions in the **Caveat** to add it to your path.
### Linux
We will assume that you know how to locate the correct packages for your distro. That said, some distros package `bundler` and `irb` separately. Make sure that you also install these.
On Arch, you will need:
- `ruby`
- `ruby-bundler`
- `ruby-irb`
- `ruby-foreman`
- `redis`
- `postgresql`
- `yarn`
- `gmp`
- `libidn`
### Windows
Unfortunately, none of the authors use Windows. Contributions welcome!
## Database
In the root of this repository, go through the following script:
```sh
# Create a folder for local data
mkdir -p data
# Set up a local database
pg_ctl -D data/postgres initdb -o '-U mastodon --auth-host=trust'
# Use the data/postgres folder for the DB connection unix socket
#
# If you don't know what that means, it's just a way for Mastodon to communicate
# with a database on the same machine efficiently.
#
# See: https://manpages.ubuntu.com/manpages/jammy/man7/unix.7.html
echo 'unix_socket_directories = .' >> data/postgres/postgresql.conf
# Start the database
pg_ctl -D data/postgres start --silent
```
## Redis
In the root of this repository, run the following:
```sh
# Create a folder for redis data
mkdir -p data/redis
# Start Redis
redis-server ./redis-dev.conf
# [Optional] Stop Redis
# kill "$(cat ./data/redis/redis-dev.pid)"
```
## Ruby
```sh
export RAILS_ENV=development
# Bundle installs all Ruby gems globally by default, which might cause problems.
bundle config set --local path 'vendor/bundle'
# [Apple Silicon] If using macOS on Apple Silicon, run the following:
# bundle config build.idn-ruby -- --with-idn-dir="$(brew --prefix libidn)"
# Install dependencies using bundle (Ruby) and yarn (JS)
bundle install
yarn install
# Setup the database using the pre-defined Rake task
#
# Rake is a command runner for Ruby projects. The `bundle exec` ensures that
# we use the version of Rake that this project requires.
bundle exec rake db:setup
# [Optional] If that fails, run the following and try again:
# bundle exec rake db:reset
```
## Running Mastodon
1. Run `export RAILS_ENV=development NODE_ENV=development`.
- Put these in your shell's .rc, or a script you can source if you want to skip this step in the future.
2. Run `bundle exec rake assets:precompile`.
- If this explodes, complaining about `Hash`, you'll need to `export NODE_OPTIONS=--openssl-legacy-provider`.
- After doing this, you will need to run `bundle exec rake assets:clobber` and then re-run `bundle exec rake assets:precompile`.
3. Run `foreman start`
# Updates/Troubleshooting
## RubyVM/DebugInspector Issues
Still unable to fix. Circumvent by removing `better_errors` and `binding_of_caller` from Gemfile.
Happy to troubleshoot with someone better with Ruby than us >_<'/.
## Webpack Issues
If Webpack compalins about being unable to find some assets or locales:
Try:
1. `rm -rf node_modules`
2. `yarn install`
If this doesn't help, try:
1. `yarn add webpack`
2. `git restore package.json yarn.lock`
3. `yarn install`
Then re-run `foreman start`. No. We have no idea why this worked.
# Need Help?
If the above instructions don't work, please contact @Rin here, or @tammy@social.treehouse.systems.

View File

@ -69,7 +69,8 @@ class Api::V1::StatusesController < Api::BaseController
content_type: status_params[:content_type], content_type: status_params[:content_type],
allowed_mentions: status_params[:allowed_mentions], allowed_mentions: status_params[:allowed_mentions],
idempotency: request.headers['Idempotency-Key'], idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true with_rate_limit: true,
quote_id: status_params[:quote_id].presence
) )
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@ -140,6 +141,7 @@ class Api::V1::StatusesController < Api::BaseController
:visibility, :visibility,
:language, :language,
:scheduled_at, :scheduled_at,
:quote_id,
:content_type, :content_type,
allowed_mentions: [], allowed_mentions: [],
media_ids: [], media_ids: [],

View File

@ -24,6 +24,7 @@ module ContextHelper
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' },
}.freeze }.freeze
def full_context def full_context

View File

@ -15,7 +15,17 @@ module FormattingHelper
module_function :extract_status_plain_text module_function :extract_status_plain_text
def status_content_format(status) def status_content_format(status)
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type) base = html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
if status.quote? && status.local?
after_html = begin
"<span class=\"quote-inline\"><a href=\"#{status.quote.to_log_permalink}\" class=\"status-link unhandled-link\" target=\"_blank\">#{status.quote.to_log_permalink}</a></span>"
end.html_safe # rubocop:disable Rails/OutputSafety
base + after_html
else
base
end
end end
def rss_status_content_format(status) def rss_status_content_format(status)

View File

@ -1,6 +0,0 @@
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
export const changeLayout = layout => ({
type: APP_LAYOUT_CHANGE,
layout,
});

View File

@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
type ChangeLayoutPayload = {
layout: 'mobile' | 'single-column' | 'multi-column';
};
export const changeLayout =
createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');

View File

@ -82,6 +82,9 @@ export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@ -135,6 +138,25 @@ export function cancelReplyCompose() {
}; };
} }
export function quoteCompose(status, router) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_QUOTE,
status: status,
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/publish');
}
};
};
export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
};
export function resetCompose() { export function resetCompose() {
return { return {
type: COMPOSE_RESET, type: COMPOSE_RESET,
@ -208,6 +230,7 @@ export function submitCompose(routerHistory) {
status, status,
content_type: getState().getIn(['compose', 'content_type']), content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
quote_id: getState().getIn(['compose', 'quote_id'], null),
media_ids: media.map(item => item.get('id')), media_ids: media.map(item => item.get('id')),
media_attributes, media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),

View File

@ -74,6 +74,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.hidden = normalOldStatus.get('hidden'); normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
} else { } else {
const spoilerText = normalStatus.spoiler_text || ''; const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
@ -83,6 +85,35 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText); normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
if (status.quote && status.quote.id) {
const quote_spoilerText = status.quote.spoiler_text || '';
const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const quote_emojiMap = makeEmojiMap(normalStatus.quote);
const quote_account_emojiMap = makeEmojiMap(status.quote.account);
const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name;
normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap);
normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent;
let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement;
Array.from(docElem.querySelectorAll('span.quote-inline'), span => span.remove());
Array.from(docElem.querySelectorAll('p,br'), line => {
let parentNode = line.parentNode;
if (line.nextSibling) {
parentNode.insertBefore(document.createTextNode(' '), line.nextSibling);
}
});
let _contentHtml = docElem.textContent;
normalStatus.quote.contentHtml = '<p>'+emojify(_contentHtml.substr(0, 150), quote_emojiMap) + (_contentHtml.substr(150) ? '...' : '')+'</p>';
normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap);
normalStatus.quote_hidden = (quote_spoilerText.length > 0 || normalStatus.quote.sensitive) && autoHideCW(settings, quote_spoilerText);
// delete the quote link!!!!
let parentDocElem = domParser.parseFromString(normalStatus.contentHtml, 'text/html').documentElement;
Array.from(parentDocElem.querySelectorAll('span.quote-inline'), span => span.remove());
normalStatus.contentHtml = parentDocElem.children[1].innerHTML;
}
} }
return normalStatus; return normalStatus;

View File

@ -71,6 +71,7 @@ class Status extends ImmutablePureComponent {
nextInReplyToId: PropTypes.string, nextInReplyToId: PropTypes.string,
rootId: PropTypes.string, rootId: PropTypes.string,
onReply: PropTypes.func, onReply: PropTypes.func,
onQuote: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onBookmark: PropTypes.func, onBookmark: PropTypes.func,
@ -713,7 +714,7 @@ class Status extends ImmutablePureComponent {
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) { if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
background = attachments.getIn([0, 'preview_url']); background = attachments.getIn([0, 'preview_url']);
} }
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) { } else if (!status.get('quote') && status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
media.push( media.push(
<Card <Card
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}

View File

@ -25,6 +25,7 @@ const messages = defineMessages({
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
@ -58,6 +59,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onQuote: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onDirect: PropTypes.func, onDirect: PropTypes.func,
onMention: PropTypes.func, onMention: PropTypes.func,
@ -124,6 +126,17 @@ class StatusActionBar extends ImmutablePureComponent {
} }
}; };
handleQuoteClick = () => {
const { signedIn } = this.context.identity;
if (signedIn) {
this.props.onQuote(this.props.status, this.context.router.history);
} else {
// TODO(ariadne): Add an interaction modal for quoting specifically.
this.props.onInteractionModal('reply', this.props.status);
}
}
handleBookmarkClick = (e) => { handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
}; };
@ -310,6 +323,8 @@ class StatusActionBar extends ImmutablePureComponent {
obfuscateCount obfuscateCount
/> />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar-button' disabled={!publicStatus} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
{shareButton} {shareButton}
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /> <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />

View File

@ -336,6 +336,37 @@ class StatusContent extends React.PureComponent {
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} /> <TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
); );
let quote = '';
if (status.get('quote', null) !== null) {
let quoteStatus = status.get('quote');
let quoteStatusContent = { __html: quoteStatus.get('contentHtml') };
let quoteStatusAccount = quoteStatus.get('account');
let quoteStatusDisplayName = { __html: quoteStatusAccount.get('display_name_html') };
quote = (
<div class="status__quote">
<blockquote>
<bdi>
<span class="quote-display-name">
<Icon
fixedWidth
id='quote-right'
aria-hidden='true'
key='icon-quote-right' />
<strong class="display-name__html">
<a onClick={this.handleAccountClick} href={quoteStatus.getIn(['account', 'url'])} dangerouslySetInnerHTML={quoteStatusDisplayName} />
</strong>
</span>
</bdi>
<div>
<a href={quoteStatus.get('url')} target='_blank' rel='noopener noreferrer' dangerouslySetInnerHTML={quoteStatusContent} />
</div>
</blockquote>
</div>
);
}
if (status.get('spoiler_text').length > 0) { if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = ''; let mentionsPlaceholder = '';
@ -401,6 +432,7 @@ class StatusContent extends React.PureComponent {
{mentionsPlaceholder} {mentionsPlaceholder}
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
{quote}
<div <div
ref={this.setContentsRef} ref={this.setContentsRef}
key={`contents-${tagLinks}`} key={`contents-${tagLinks}`}
@ -426,6 +458,7 @@ class StatusContent extends React.PureComponent {
onMouseUp={this.handleMouseUp} onMouseUp={this.handleMouseUp}
tabIndex={0} tabIndex={0}
> >
{quote}
<div <div
ref={this.setContentsRef} ref={this.setContentsRef}
key={`contents-${tagLinks}-${rewriteMentions}`} key={`contents-${tagLinks}-${rewriteMentions}`}
@ -447,6 +480,7 @@ class StatusContent extends React.PureComponent {
className='status__content' className='status__content'
tabIndex={0} tabIndex={0}
> >
{quote}
<div <div
ref={this.setContentsRef} ref={this.setContentsRef}
key={`contents-${tagLinks}`} key={`contents-${tagLinks}`}

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import configureStore from 'flavours/glitch/store/configureStore'; import { store } from 'flavours/glitch/store/configureStore';
import { hydrateStore } from 'flavours/glitch/actions/store'; import { hydrateStore } from 'flavours/glitch/actions/store';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from 'mastodon/locales'; import { getLocale } from 'mastodon/locales';
@ -12,8 +12,6 @@ import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
addLocaleData(localeData); addLocaleData(localeData);
const store = configureStore();
if (initialState) { if (initialState) {
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }

View File

@ -3,6 +3,7 @@ import Status from 'flavours/glitch/components/status';
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import { import {
replyCompose, replyCompose,
quoteCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
} from 'flavours/glitch/actions/compose'; } from 'flavours/glitch/actions/compose';
@ -49,6 +50,8 @@ const messages = defineMessages({
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' }, editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
@ -108,6 +111,23 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}); });
}, },
onQuote (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
onConfirm: () => dispatch(quoteCompose(status, router)),
}));
} else {
dispatch(quoteCompose(status, router));
}
});
},
onModalReblog (status, privacy) { onModalReblog (status, privacy) {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));

View File

@ -3,6 +3,7 @@ import CharacterCounter from './character_counter';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import QuoteIndicatorContainer from '../containers/quote_indicator_container';
import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import AutosuggestInput from '../../../components/autosuggest_input'; import AutosuggestInput from '../../../components/autosuggest_input';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
@ -309,6 +310,7 @@ class ComposeForm extends ImmutablePureComponent {
<WarningContainer /> <WarningContainer />
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
<QuoteIndicatorContainer />
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}> <div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput <AutosuggestInput

View File

@ -39,6 +39,8 @@ const notFoundFn = () => (
set='twitter' set='twitter'
size={32} size={32}
sheetSize={32} sheetSize={32}
sheetColumns={60}
sheetRows={60}
backgroundImageFn={backgroundImageFn} backgroundImageFn={backgroundImageFn}
/> />
@ -97,12 +99,12 @@ class ModifierPickerMenu extends React.PureComponent {
return ( return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
<button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
</div> </div>
); );
} }
@ -137,7 +139,7 @@ class ModifierPicker extends React.PureComponent {
return ( return (
<div className='emoji-picker-dropdown__modifiers'> <div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /> <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div> </div>
); );
@ -274,6 +276,8 @@ class EmojiPickerMenuImpl extends React.PureComponent {
perLine={8} perLine={8}
emojiSize={22} emojiSize={22}
sheetSize={32} sheetSize={32}
sheetColumns={60}
sheetRows={60}
custom={buildCustomEmojis(custom_emojis)} custom={buildCustomEmojis(custom_emojis)}
color='' color=''
emoji='' emoji=''

View File

@ -0,0 +1,87 @@
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
// Components.
import AccountContainer from 'flavours/glitch/containers/account_container';
import Icon from 'flavours/glitch/components/icon';
import IconButton from 'flavours/glitch/components/icon_button';
import AttachmentList from 'flavours/glitch/components/attachment_list';
// Messages.
const messages = defineMessages({
cancel: {
defaultMessage: 'Cancel',
id: 'quote_indicator.cancel',
},
});
class QuoteIndicator extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
onCancel: PropTypes.func,
};
handleClick = () => {
const { onCancel } = this.props;
if (onCancel) {
onCancel();
}
}
// Rendering.
render () {
const { status, intl } = this.props;
if (!status) {
return null;
}
const account = status.get('account');
const content = status.get('content');
const attachments = status.get('media_attachments');
// The result.
return (
<article className='quote-indicator'>
<header className='quote-indicator__header'>
<IconButton
className='quote-indicator__cancel'
icon='times'
onClick={this.handleClick}
title={intl.formatMessage(messages.cancel)}
inverted
/>
<Icon
className='quote-indicator__cancel icon-button inverted'
id='quote-right' />
{account && (
<AccountContainer
id={account}
small
/>
)}
</header>
<div
className='quote-indicator__content icon-button translate'
dangerouslySetInnerHTML={{ __html: content || '' }}
/>
{attachments.size > 0 && (
<AttachmentList
compact
media={attachments}
/>
)}
</article>
);
}
}
export default injectIntl(QuoteIndicator)

View File

@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
// Components. // Components.
import AccountContainer from 'flavours/glitch/containers/account_container'; import AccountContainer from 'flavours/glitch/containers/account_container';
import Icon from 'flavours/glitch/components/icon';
import IconButton from 'flavours/glitch/components/icon_button'; import IconButton from 'flavours/glitch/components/icon_button';
import AttachmentList from 'flavours/glitch/components/attachment_list'; import AttachmentList from 'flavours/glitch/components/attachment_list';
@ -57,6 +58,9 @@ class ReplyIndicator extends ImmutablePureComponent {
title={intl.formatMessage(messages.cancel)} title={intl.formatMessage(messages.cancel)}
inverted inverted
/> />
<Icon
className='quote-indicator__cancel icon-button inverted'
id='reply' />
{account && ( {account && (
<AccountContainer <AccountContainer
id={account} id={account}

View File

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { cancelQuoteCompose } from 'flavours/glitch/actions/compose';
import QuoteIndicator from '../components/quote_indicator';
const makeMapStateToProps = () => {
const mapStateToProps = state => {
const statusId = state.getIn(['compose', 'quote_id']);
const editing = false;
return {
status: state.getIn(['statuses', statusId]),
editing,
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({
onCancel () {
dispatch(cancelQuoteCompose());
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(QuoteIndicator);

View File

@ -18,6 +18,7 @@ const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent {
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired,
onQuote: PropTypes.func.isRequired,
onMute: PropTypes.func, onMute: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onBlock: PropTypes.func, onBlock: PropTypes.func,
@ -81,6 +83,10 @@ class ActionBar extends React.PureComponent {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
}; };
handleQuoteClick = () => {
this.props.onQuote(this.props.status);
}
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
}; };
@ -215,6 +221,7 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='quote-right-icon' disabled={!publicStatus} title={intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>

View File

@ -220,7 +220,7 @@ class DetailedStatus extends ImmutablePureComponent {
); );
mediaIcons.push('picture-o'); mediaIcons.push('picture-o');
} }
} else if (status.get('card')) { } else if (!status.get('quote') && status.get('card')) {
media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />); media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
mediaIcons.push('link'); mediaIcons.push('link');
} }

View File

@ -3,6 +3,7 @@ import DetailedStatus from '../components/detailed_status';
import { makeGetStatus } from 'flavours/glitch/selectors'; import { makeGetStatus } from 'flavours/glitch/selectors';
import { import {
replyCompose, replyCompose,
quoteCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
} from 'flavours/glitch/actions/compose'; } from 'flavours/glitch/actions/compose';
@ -31,6 +32,8 @@ import { showAlertForError } from 'flavours/glitch/actions/alerts';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
@ -66,6 +69,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}); });
}, },
onQuote (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.quoteMessage),
confirm: intl.formatMessage(messages.quoteConfirm),
onConfirm: () => dispatch(quoteCompose(status, router)),
}));
} else {
dispatch(quoteCompose(status, router));
}
});
},
onModalReblog (status, privacy) { onModalReblog (status, privacy) {
dispatch(reblog(status, privacy)); dispatch(reblog(status, privacy));
}, },

View File

@ -32,6 +32,7 @@ import {
} from 'flavours/glitch/actions/interactions'; } from 'flavours/glitch/actions/interactions';
import { import {
replyCompose, replyCompose,
quoteCompose,
mentionCompose, mentionCompose,
directCompose, directCompose,
} from 'flavours/glitch/actions/compose'; } from 'flavours/glitch/actions/compose';
@ -327,6 +328,21 @@ class Status extends ImmutablePureComponent {
} }
}; };
handleQuoteClick = (status) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
dispatch(quoteCompose(status, this.context.router.history));
} else {
dispatch(openModal('INTERACTION', {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
}));
}
}
handleModalReblog = (status, privacy) => { handleModalReblog = (status, privacy) => {
const { dispatch } = this.props; const { dispatch } = this.props;
@ -698,6 +714,7 @@ class Status extends ImmutablePureComponent {
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick} onBookmark={this.handleBookmarkClick}
onQuote={this.handleQuoteClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick} onEdit={this.handleEditClick}
onDirect={this.handleDirectClick} onDirect={this.handleDirectClick}

View File

@ -378,7 +378,7 @@ class UI extends React.Component {
if (layout !== this.props.layout) { if (layout !== this.props.layout) {
this.handleLayoutChange.cancel(); this.handleLayoutChange.cancel();
this.props.dispatch(changeLayout(layout)); this.props.dispatch(changeLayout({ layout }));
} else { } else {
this.handleLayoutChange(); this.handleLayoutChange();
} }

View File

@ -6,6 +6,8 @@ import {
COMPOSE_REPLY, COMPOSE_REPLY,
COMPOSE_REPLY_CANCEL, COMPOSE_REPLY_CANCEL,
COMPOSE_DIRECT, COMPOSE_DIRECT,
COMPOSE_QUOTE,
COMPOSE_QUOTE_CANCEL,
COMPOSE_MENTION, COMPOSE_MENTION,
COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_REQUEST,
COMPOSE_SUBMIT_SUCCESS, COMPOSE_SUBMIT_SUCCESS,
@ -85,6 +87,7 @@ const initialState = ImmutableMap({
caretPosition: null, caretPosition: null,
preselectDate: null, preselectDate: null,
in_reply_to: null, in_reply_to: null,
quote_id: null,
is_submitting: false, is_submitting: false,
is_uploading: false, is_uploading: false,
is_changing_upload: false, is_changing_upload: false,
@ -173,6 +176,7 @@ function clearAll(state) {
map.set('is_submitting', false); map.set('is_submitting', false);
map.set('is_changing_upload', false); map.set('is_changing_upload', false);
map.set('in_reply_to', null); map.set('in_reply_to', null);
map.set('quote_id', null);
map.update( map.update(
'advanced_options', 'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options')), map => map.mergeWith(overwrite, state.get('default_advanced_options')),
@ -361,6 +365,51 @@ const updateSuggestionTags = (state, token) => {
}); });
}; };
const updateWithReply = (state, action) => {
// doesn't support QT&reply
const isQuote = action.type === COMPOSE_QUOTE;
const parentStatusId = action.status.get('id');
return state.withMutations(map => {
map.set('id', null);
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
);
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
if (action.status.get('spoiler_text').length > 0) {
let spoilerText = action.status.get('spoiler_text');
if (action.prependCWRe && !spoilerText.match(/^(re|qt)[: ]/i)) {
spoilerText = isQuote ? `QT: ${spoilerText}` : `re: ${spoilerText}`;
}
map.set('spoiler', true);
map.set('spoiler_text', spoilerText);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
if (isQuote) {
map.set('in_reply_to', null);
map.set('quote_id', parentStatusId);
map.set('text', '');
} else {
map.set('in_reply_to', parentStatusId);
map.set('quote_id', null);
map.set('text', statusToTextMentions(state, action.status));
if (action.status.get('language')) {
map.set('language', action.status.get('language'));
}
}
});
};
export default function compose(state = initialState, action) { export default function compose(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STORE_HYDRATE: case STORE_HYDRATE:
@ -410,46 +459,17 @@ export default function compose(state = initialState, action) {
return state return state
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends); .set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
case COMPOSE_REPLY: case COMPOSE_REPLY:
return state.withMutations(map => { case COMPOSE_QUOTE:
map.set('id', null); return updateWithReply(state, action);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
);
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.update('media_attachments', list => list.filter(media => media.get('unattached')));
if (action.status.get('language') && !action.status.has('translation')) {
map.set('language', action.status.get('language'));
} else {
map.set('language', state.get('default_language'));
}
if (action.status.get('spoiler_text').length > 0) {
let spoiler_text = action.status.get('spoiler_text');
if (action.prependCWRe && !spoiler_text.match(/^re[: ]/i)) {
spoiler_text = 're: '.concat(spoiler_text);
}
map.set('spoiler', true);
map.set('spoiler_text', spoiler_text);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
});
case COMPOSE_REPLY_CANCEL: case COMPOSE_REPLY_CANCEL:
state = state.setIn(['advanced_options', 'threaded_mode'], false); state = state.setIn(['advanced_options', 'threaded_mode'], false);
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended // eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
case COMPOSE_QUOTE_CANCEL:
// eslint-disable-next-line no-fallthrough -- fall-through to `COMPOSE_RESET` is intended
case COMPOSE_RESET: case COMPOSE_RESET:
return state.withMutations(map => { return state.withMutations(map => {
map.set('in_reply_to', null); map.set('in_reply_to', null);
map.set('quote_id', null);
if (defaultContentType) map.set('content_type', defaultContentType); if (defaultContentType) map.set('content_type', defaultContentType);
map.set('text', ''); map.set('text', '');
map.set('spoiler', false); map.set('spoiler', false);
@ -503,6 +523,7 @@ export default function compose(state = initialState, action) {
return item; return item;
})); }));
case INIT_MEDIA_EDIT_MODAL: case INIT_MEDIA_EDIT_MODAL:
{
const media = state.get('media_attachments').find(item => item.get('id') === action.id); const media = state.get('media_attachments').find(item => item.get('id') === action.id);
return state.set('media_modal', ImmutableMap({ return state.set('media_modal', ImmutableMap({
id: action.id, id: action.id,
@ -511,6 +532,7 @@ export default function compose(state = initialState, action) {
focusY: media.getIn(['meta', 'focus', 'y'], 0), focusY: media.getIn(['meta', 'focus', 'y'], 0),
dirty: false, dirty: false,
})); }));
}
case COMPOSE_CHANGE_MEDIA_DESCRIPTION: case COMPOSE_CHANGE_MEDIA_DESCRIPTION:
return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true); return state.setIn(['media_modal', 'description'], action.description).setIn(['media_modal', 'dirty'], true);
case COMPOSE_CHANGE_MEDIA_FOCUS: case COMPOSE_CHANGE_MEDIA_FOCUS:
@ -566,6 +588,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_DOODLE_SET: case COMPOSE_DOODLE_SET:
return state.mergeIn(['doodle'], action.options); return state.mergeIn(['doodle'], action.options);
case REDRAFT: case REDRAFT:
{
const do_not_federate = !!action.status.get('local_only'); const do_not_federate = !!action.status.get('local_only');
let text = action.raw_text || unescapeHTML(expandMentions(action.status)); let text = action.raw_text || unescapeHTML(expandMentions(action.status));
if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, ''); if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, '');
@ -606,6 +629,7 @@ export default function compose(state = initialState, action) {
})); }));
} }
}); });
}
case COMPOSE_SET_STATUS: case COMPOSE_SET_STATUS:
return state.withMutations(map => { return state.withMutations(map => {
map.set('id', action.status.get('id')); map.set('id', action.status.get('id'));

View File

@ -1,5 +1,5 @@
import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
import { APP_LAYOUT_CHANGE } from 'flavours/glitch/actions/app'; import { changeLayout } from 'flavours/glitch/actions/app';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { layoutFromWindow } from 'flavours/glitch/is_mobile';
@ -16,8 +16,8 @@ export default function meta(state = initialState, action) {
return state.merge(action.state.get('meta')) return state.merge(action.state.get('meta'))
.set('permissions', action.state.getIn(['role', 'permissions'])) .set('permissions', action.state.getIn(['role', 'permissions']))
.set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout']))); .set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])));
case APP_LAYOUT_CHANGE: case changeLayout.type:
return state.set('layout', action.layout); return state.set('layout', action.payload.layout);
default: default:
return state; return state;
} }

View File

@ -128,6 +128,7 @@
} }
} }
.quote-indicator,
.reply-indicator { .reply-indicator {
margin: 0 0 10px; margin: 0 0 10px;
border-radius: 4px; border-radius: 4px;
@ -138,6 +139,7 @@
flex: 0 2 auto; flex: 0 2 auto;
} }
.quote-indicator__header,
.reply-indicator__header { .reply-indicator__header {
margin-bottom: 5px; margin-bottom: 5px;
overflow: hidden; overflow: hidden;
@ -147,11 +149,13 @@
} }
} }
.quote-indicator__cancel,
.reply-indicator__cancel { .reply-indicator__cancel {
float: right; float: right;
line-height: 24px; line-height: 24px;
} }
.quote-indicator__content,
.reply-indicator__content { .reply-indicator__content {
position: relative; position: relative;
font-size: 14px; font-size: 14px;

View File

@ -68,7 +68,8 @@
} }
p, p,
pre { pre,
blockquote {
margin-bottom: 20px; margin-bottom: 20px;
white-space: pre-wrap; white-space: pre-wrap;
unicode-bidi: plaintext; unicode-bidi: plaintext;
@ -78,6 +79,99 @@
} }
} }
.status__quote {
padding-bottom: 0.5em;
}
.status__quote,
.status__content__text,
.e-content {
overflow: hidden;
& > ul,
& > ol {
margin-bottom: 20px;
}
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 1.2em;
}
h2 {
font-size: 1.1em;
}
h3,
h4,
h5 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
i[role=img] {
font-style: normal;
padding-right: 0.25em;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 2em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
}
a { a {
color: $secondary-text-color; color: $secondary-text-color;
text-decoration: none; text-decoration: none;
@ -212,23 +306,28 @@
} }
} }
@mixin focusable {
outline: 0;
background: lighten($ui-base-color, 4%);
&.status.status-direct {
background: lighten($ui-base-color, 12%);
&.muted {
background: transparent;
}
}
.detailed-status,
.detailed-status__action-bar {
background: lighten($ui-base-color, 8%);
}
}
.focusable { .focusable {
&:focus { &:focus,
outline: 0; &:hover {
background: lighten($ui-base-color, 4%); @include focusable;
&.status.status-direct {
background: lighten($ui-base-color, 12%);
&.muted {
background: transparent;
}
}
.detailed-status,
.detailed-status__action-bar {
background: lighten($ui-base-color, 8%);
}
} }
} }
@ -638,6 +737,7 @@
} }
a.status__display-name, a.status__display-name,
.quote-indicator__display-name,
.reply-indicator__display-name, .reply-indicator__display-name,
.detailed-status__display-name, .detailed-status__display-name,
.account__display-name { .account__display-name {

View File

@ -14,6 +14,7 @@
.status__content a, .status__content a,
.link-footer a, .link-footer a,
.quote-indicator__content a,
.reply-indicator__content a, .reply-indicator__content a,
.status__content__read-more-button { .status__content__read-more-button {
text-decoration: underline; text-decoration: underline;

View File

@ -273,6 +273,7 @@ html {
} }
// Change the background colors of status__content__spoiler-link // Change the background colors of status__content__spoiler-link
.quote-indicator__content .status__content__spoiler-link,
.reply-indicator__content .status__content__spoiler-link, .reply-indicator__content .status__content__spoiler-link,
.status__content .status__content__spoiler-link { .status__content .status__content__spoiler-link {
background: $ui-base-color; background: $ui-base-color;
@ -648,6 +649,7 @@ html {
} }
} }
.quote-indicator,
.reply-indicator { .reply-indicator {
background: transparent; background: transparent;
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 8%);
@ -659,6 +661,7 @@ html {
} }
.status__content, .status__content,
.quote-indicator__content,
.reply-indicator__content { .reply-indicator__content {
a { a {
color: $highlight-text-color; color: $highlight-text-color;

View File

@ -1,9 +1,10 @@
import type { MastodonMap } from './util'; import type { Record } from 'immutable';
type AccountValues = { type AccountValues = {
id: number; id: number;
avatar: string; avatar: string;
avatar_static: string; avatar_static: string;
[key: string]: any; [key: string]: any;
} };
export type Account = MastodonMap<AccountValues>
export type Account = Record<AccountValues>;

View File

@ -1,7 +1 @@
export interface MastodonMap<T> {
get<K extends keyof T>(key: K): T[K];
has<K extends keyof T>(key: K): boolean;
set<K extends keyof T>(key: K, value: T[K]): this;
}
export type ValueOf<T> = T[keyof T]; export type ValueOf<T> = T[keyof T];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,2 +1,138 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg299"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
sodipodi:docname="treehouse-icon-wordmark.svg"
xml:space="preserve"
inkscape:export-filename="safari-1024x1024.png"
inkscape:export-xdpi="260.10001"
inkscape:export-ydpi="260.10001"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview301"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.1739703"
inkscape:cx="171.11549"
inkscape:cy="141.21628"
inkscape:window-width="1623"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs296"><linearGradient
id="linearGradient34140"
inkscape:swatch="solid"><stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop34138" /></linearGradient><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath266"><rect
style="fill:#000000;stroke-width:0.75"
id="rect268"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath262"><rect
style="fill:#000000;stroke-width:0.75"
id="rect264"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath258"><rect
style="fill:#000000;stroke-width:0.75"
id="rect260"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath254"><rect
style="fill:#000000;stroke-width:0.75"
id="rect256"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath250"><rect
style="fill:#000000;stroke-width:0.75"
id="rect252"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath246"><rect
style="fill:#000000;stroke-width:0.75"
id="rect248"
width="300"
height="300"
x="0.89256281"
y="-298.39465"
transform="scale(1,-1)" /></clipPath><mask
maskUnits="userSpaceOnUse"
id="mask1677"><image
width="100"
height="100"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAM4AAADOCAYAAAB2Hz3EAAAABHNCSVQICAgIfAhkiAAACLpJREFU eJzt3W2MXFUdx/Hv3rZS2l3a2tqUllZiaMFqAtQUiBIfXqClaEKiMRoFDFgjhJg0xhgVE40afaGS gBHxkQokRmLUGLQqvjBqlIoIAY2lVdSU1kK7LWX73DK+ODPs7HZ2985/7sydu/v9JDf7NHPu/8X+ 8j9799xzB2q1Gn1uKbCifiwHXgEsrB8LgEFgDnAOMKv+npcB83OOP1AfqwhzgbMLGqtoh4ETBY01 ApzM+drjwJGmrw8CL9Y/Hq5/PAAMA7uAvcB/gd1A3/5yDvRRcM4GLgEuA9YBFwFrKO6XWtVyFHgK 2AE8BmyrH8+XWVRDmcEZIAVlA3A1cAWpc0gTqQFPAlvrx+/I3/kKVUZwLgLeD1wHrOr1yTWtDAMP AFuAP/byxL0KTgZcC2wGruzFCTXj/AO4Hfg+cKzbJ+t2cGYDNwAfAy7s5omkur3AHfVjpFsn6WZw NgBfAdZ26wTSJPYAtwH3kK7iFaobwVkJ3AVcU/TAUsCjwE2kK3OFyQoca4BU4BMYGvWPdaTL2J+h wKu2RXWcxaSW+PYiBpO65BHg3cDTnQ5URHAuBn4CnN/pQFIPHATeA/yyk0E6naptAH6PoVF1LAQe BG7tZJBOgvNe4KektWJSlcwC7gQ+Hx0gGpxNwH2kxZRSVX0K+GrkjZHgvI90ubnIK3JSWTYDn273 Te1eHNgI/Bg7jaafW0gNIZd2gvMa4GHy3+ciVclp0r9TtuZ5cd7gLAD+DKyO1yX1vWFgPfCvqV6Y 5++UAeBeDI2mv5cDPyLdyTupPMG5EXhHpxVJFXEJ8NmpXjTVVG0Vae3ZOQUVJVXBadJ9Y3+a6AVT dZy7MTSaeWYB32WSRaGTBedq0pIaaSZ6NXDzRD+caKo2G3gcb0LTzLYfuIC0MHSMiTrODRgaaTHw iVY/aNVxMmA7KWnSTDdCWv2/v/mbrTrOtRgaqWGQFn/rtOo4fwBe34uKpIrYDbwSONX4xviOcyGG RhpvOfDW5m+MD871vatFqpQx2Wieqg0A/yFt7yRprGPAMuqbvjd3nPUYGmkic4GrGl80B2dj72uR KuVtjU+ag3NVixdKGvVSc2n8jTOXNHfzlmhpcquBnY2OczGGRspjPYxO1a4osRCpSi6H0eBcWmIh UpWsg9HgrCmxEKlKVsPoxYFhYFGp5UjVsSgDlmBopHZckAEryq5CqpiVGWnlp6T8zjU4UvuWZaT7 qiXltyTDB0NJ7RrKgKGyq5AqZtCOI7XPjiMF2HGkgKGM9NAoSfkN+gBcqX1ZRo6nT0kaYygDziq7 CqlqnKpJAQZHat/sDJhXdhVSxczPmOQ5h5Jac6omBWSkJ+xKaoNLbqQAp2pSgMGRAgZqtdppDJDU lgxDI7XN0EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUY HCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA 4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgBBkcKMDhSgMGRAgyOFGBwpACDIwUYHCnA4EgB BkcKMDhSgMGRAgyOFGBwpACDIwVkwOGyi5CqJgNOlV2EVDVO1aQAgyMFGBwpwOBIAQZHCjA4UoDB kQIMjhRgcKQAgyMFGBwpIAOOlF2EVDUZcLLsIqSqcaomBRgcKcDgSAEGRwowOFKAwZECDI4UYHCk AIMjBRgcKcDgSAEGRwowOFKAwZECvK1ACvBGNql9h52qSe07ZXCkgAw4UXYRUtVkwNGyi5AqZsSp mtS+0xlwqOwqpIoZyYAXyq5CqpiRDBgpuwqpYuw4UsALdhypfXYcKeBQBuwvuwqpYoYz4H9lVyFV zJ4M2F12FVLF7MmAXWVXIVXMMwO1Wg3S6oGhkouRqmJxY63azlLLkKrjOeoXBwD+VmYlUoU8BaOb dTxeYiFSlTwGo8F5tMRCpCp5GEaDsw04VV4tUmVsg9HgjAB/Ka8WqRKeZdzfOAC/KacWqTJ+AdRg bHAeLKcWqTK2Nj5p/AMUUoieAZaVUZHU506QsnEAxnacF4H7yqhIqoCfUw8NnLnp+r29rUWqjC3N XzRP1RoeAV7Xs3Kk/rcPWEHT5p2t9lW7vWflSNVwB+N2vG3VcWYD/wRW9agoqZ8dBs4ndZ2XtOo4 p4Av96AgqQq+w7jQQOuOA3AW8HfgVV0uSupnh4DVpBUDY0y0d/Rx4OPdrEiqgC/SIjQwccdp+C3w xm5UJPW5p4G1wLFWP5zqaQUfxMeAaOapAZuYIDQwdXB2ALcVWZFUAXczxaLnqaZqkML1EPCWgoqS +tlO4FKm2Bo6T3AAlpLu1zmv87qkvnUUeAPw16lemPeJbM8C78LnhWp620SO0ED+4EC61/pG0ipq abr5EnB/3hfnnao1uxn4ertvkvrYt4EPUb+7M4/Iw3PvAj4ZeJ/Uj7YAH6aN0ECs4zTcSlo1OhAd QCrZN4FbgNPtvrGTx7V/DbgOONnBGFJZPkfqNG2HBjrrOA1XAj8Ezu10IKkHRkhXz37QySBFBAdS aB4gXQOX+tUO4J3AE50O1MlUrdke4M2k5Tn+r0f9pgZ8g7QlQMehgeI6TrPXAvfgvgXqD/8mTc0e KnLQojpOsyeBy4CbSJ1IKsMIaQa0loJDA93pOM0GgY8Am4El3TyRVHcE+BbwBdJDoLqi28FpmAd8 APgo3o6t7tgH3Ela1XLGHgFF61VwGjLgTcD1pEWjg708uaadk6QdNr9H2tf5eK9O3OvgNJtHuhJ3 DbCRtAWPNJXngF8BPwN+DQyXUUSZwRlvJemiwuXAOmBN/XuaufYB20mPD9xWP7bT5rqybuin4LQy n7Q9z3nA8vqxFFhYPxYBc4AFpGngEGlDxYY5OB0syzHO3K+isWn586T9+w7WjwOkTrIX2AXsJv2z spRuksf/AQGJjDhvlYqLAAAAAElFTkSuQmCC "
id="image1679"
x="-142.08125"
y="-68.527077"
style="fill:#000000;fill-opacity:1" /></mask></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"><g
id="g9734"
transform="translate(-0.56747079,-0.60319788)"
style="fill:#000000;fill-opacity:1"><path
id="path9722"
clip-path="url(#clipPath266)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.04672"
d="m 148.10952,261.04452 c -2.06649,0.0422 -4.13783,0.0165 -6.20654,-0.082 -9.03297,-0.43028 -17.4723,-1.69278 -25.21582,-3.6709 1.95723,-3.04579 3.62861,-6.19047 4.86181,-9.41895 1.0933,-2.86169 2.06819,-6.06832 2.93702,-9.39404 5.97882,1.41185 12.41455,2.26761 19.3374,2.43164 7.8224,0.18531 15.14923,-0.68835 21.97412,-2.31592 0.86115,3.28373 1.82434,6.45027 2.90479,9.27832 1.23038,3.22113 2.88552,6.3635 4.83691,9.40284 -7.87565,2.15196 -16.58364,3.58862 -25.42969,3.76904 z M 86.219871,243.54843 C 79.135999,238.83894 72.903758,233.56337 67.024558,227.68417 49.557998,210.21762 36.413306,184.82735 36.943991,150.03133 37.322903,125.20846 46.732098,105.05671 57.637839,90.085532 61.45103,84.852994 65.425291,79.543181 70.00991,75.366781 c 19.181088,-17.465513 42.57142,-30.269605 77.8667,-30.079102 15.91534,0.08478 31.16013,4.698278 43.30518,10.453125 12.23927,5.799859 22.86841,13.510545 31.35937,22.612794 8.56947,9.187033 15.97962,19.660028 21.33252,32.425782 5.2315,12.47583 9.54041,27.78413 8.74805,45.22559 -0.73166,16.10163 -4.59718,31.20723 -10.45459,43.30517 -8.99238,18.57589 -21.58401,33.19172 -38.1958,44.06397 -2.31466,-6.63911 -4.61328,-13.17798 -6.85254,-19.50586 3.44267,-2.57487 6.59968,-5.30936 9.42187,-8.13135 14.29397,-14.29395 25.48515,-34.26814 26.02735,-61.65234 0.54115,-27.4062 -11.35774,-48.68927 -25.17334,-62.504888 -14.30339,-14.304424 -34.60695,-26.239746 -61.43848,-26.239746 -27.74113,0 -48.035409,10.491046 -62.718751,25.17334 -13.699435,13.699434 -26.239746,34.165224 -26.239746,61.438474 0,27.79034 10.744976,48.07693 25.171875,62.50489 3.314031,3.31403 6.965461,6.49069 10.954102,9.43213 -2.257102,6.37839 -4.571641,12.9692 -6.903809,19.65967 z"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)" /><g
id="g9726"
clip-path="url(#clipPath262)"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)"
style="fill:#000000;fill-opacity:1"><path
id="path9724"
style="color:#000000;fill:#000000;stroke-width:7.79528;-inkscape-stroke:none;fill-opacity:1"
d="m 75.314109,288.61142 c 0.792777,-2.69314 1.634814,-5.51875 2.904786,-9.4292 3.288523,-10.12592 7.91432,-23.60109 12.613769,-37.04883 8.510354,-24.35287 15.714366,-44.24376 17.293946,-48.61524 l 17.83447,13.95117 c -0.34406,3.20433 -2.77605,25.20128 -8.05224,39.01172 v 0.001 c -2.69383,7.05234 -7.75838,14.01594 -13.73438,20.22656 -7.323043,7.61056 -15.978751,14.09202 -22.936522,18.52002 -2.358994,1.50129 -4.088107,2.38872 -5.923829,3.38233 z" /></g><g
id="g9730"
clip-path="url(#clipPath258)"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)"
style="fill:#000000;fill-opacity:1"><path
id="path9728"
style="color:#000000;fill:#000000;stroke-width:7.79528;-inkscape-stroke:none;fill-opacity:1"
d="m 214.98257,288.76376 c -0.13295,-0.0727 -0.19067,-0.0887 -0.32666,-0.16406 -3.98394,-2.20699 -9.28528,-5.56377 -14.80371,-9.75147 -11.03686,-8.37541 -22.86033,-20.19752 -27.5083,-32.36572 v -0.001 c -5.27619,-13.81042 -7.70819,-35.80734 -8.05225,-39.01172 l 17.83301,-13.95117 c 1.5796,4.37153 8.7836,24.2624 17.29394,48.61524 4.69945,13.44774 9.32378,26.92291 12.61231,37.04883 1.29043,3.97344 2.15527,6.86878 2.95166,9.58154 z" /></g><path
d="m 173.4755,165.60476 c -0.4773,6.26252 -0.12456,18.21709 -3.41335,22.39978 -2.90046,3.68864 -10.18248,3.58187 -17.06675,3.83936 -6.86752,0.2575 -12.48631,0.65316 -17.91878,0 -5.70462,-0.6856 -11.77872,-0.0314 -14.93354,-3.41335 -3.47091,-3.72108 -3.30135,-14.73047 -3.83936,-21.97272 -1.10533,-14.86654 -0.58616,-29.35627 -13.86693,-31.14616 0.12247,-1.65486 -0.25749,-3.8132 0.21353,-5.1195 27.39054,-0.17271 56.5605,-0.48045 84.47858,0 0.3203,1.41097 0.3203,3.70852 0,5.1195 -13.07247,1.98981 -12.54074,15.70601 -13.6534,30.29309 m -40.53209,59.51852 h 24.31946 c 0.49301,-3.98695 0.0743,-8.8856 0.21249,-13.22634 12.614,-1.12731 25.68962,-0.0429 31.35969,-7.8933 4.38052,-6.06469 4.12303,-17.83295 4.90702,-28.15988 1.35027,-17.79945 0.3496,-34.36692 18.34584,-35.41154 -0.2397,-9.97523 0.50661,-21.91934 -0.42706,-30.50662 h -54.39798 c -0.62908,-4.70396 0.20307,-10.87018 -0.42706,-15.57308 -8.03671,0.1413 -16.63864,-0.28262 -24.31946,0.21353 -0.49405,4.62545 -0.0733,10.16573 -0.21353,15.14602 -18.17209,0.56523 -38.38841,-0.10153 -54.825041,0.42706 v 30.50662 c 5.375951,0.63012 10.631521,2.43153 13.438821,6.39964 3.98172,5.62506 4.12408,16.23356 4.90702,25.81208 1.21629,14.87806 0.30041,30.53069 10.66607,35.62612 6.43104,3.16109 15.7573,2.71624 25.81313,3.19982 0.75259,3.9409 -0.45951,9.84648 0.64059,13.43987"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.75;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path9732"
clip-path="url(#clipPath254)"
transform="matrix(0.35277777,0,0,-0.35277777,-0.52260784,108.57872)" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -38,6 +38,8 @@ const notFoundFn = () => (
set='twitter' set='twitter'
size={32} size={32}
sheetSize={32} sheetSize={32}
sheetColumns={60}
sheetRows={60}
backgroundImageFn={backgroundImageFn} backgroundImageFn={backgroundImageFn}
/> />
@ -96,12 +98,12 @@ class ModifierPickerMenu extends React.PureComponent {
return ( return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div> </div>
); );
} }
@ -136,7 +138,7 @@ class ModifierPicker extends React.PureComponent {
return ( return (
<div className='emoji-picker-dropdown__modifiers'> <div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} sheetColumns={60} sheetRows={60} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div> </div>
); );
@ -273,6 +275,8 @@ class EmojiPickerMenuImpl extends React.PureComponent {
perLine={8} perLine={8}
emojiSize={22} emojiSize={22}
sheetSize={32} sheetSize={32}
sheetColumns={60}
sheetRows={60}
custom={buildCustomEmojis(custom_emojis)} custom={buildCustomEmojis(custom_emojis)}
color='' color=''
emoji='' emoji=''

View File

@ -108,24 +108,27 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def process_status_params def process_status_params
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url) @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url)
@params = { @params = begin
uri: @status_parser.uri, {
url: @status_parser.url || @status_parser.uri, uri: @status_parser.uri,
account: @account, url: @status_parser.url || @status_parser.uri,
text: converted_object_type? ? converted_text : (@status_parser.text || ''), account: @account,
language: @status_parser.language, text: converted_object_type? ? converted_text : (@status_parser.text || ''),
spoiler_text: converted_object_type? ? '' : (@status_parser.spoiler_text || ''), language: @status_parser.language,
created_at: @status_parser.created_at, spoiler_text: converted_object_type? ? '' : (@status_parser.spoiler_text || ''),
edited_at: @status_parser.edited_at && @status_parser.edited_at != @status_parser.created_at ? @status_parser.edited_at : nil, created_at: @status_parser.created_at,
override_timestamps: @options[:override_timestamps], edited_at: @status_parser.edited_at && @status_parser.edited_at != @status_parser.created_at ? @status_parser.edited_at : nil,
reply: @status_parser.reply, override_timestamps: @options[:override_timestamps],
sensitive: @account.sensitized? || @status_parser.sensitive || false, reply: @status_parser.reply,
visibility: @status_parser.visibility, sensitive: @account.sensitized? || @status_parser.sensitive || false,
thread: replied_to_status, visibility: @status_parser.visibility,
conversation: conversation_from_uri(@object['conversation']), thread: replied_to_status,
media_attachment_ids: process_attachments.take(4).map(&:id), conversation: conversation_from_uri(@object['conversation']),
poll: process_poll, media_attachment_ids: process_attachments.take(4).map(&:id),
} poll: process_poll,
quote: process_quote,
}
end
end end
def process_audience def process_audience
@ -424,4 +427,28 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
poll.reload poll.reload
retry retry
end end
def guess_quote_url
if @object["quoteUri"] && !@object["quoteUri"].empty?
@object["quoteUri"]
elsif @object["quoteUrl"] && !@object["quoteUrl"].empty?
@object["quoteUrl"]
elsif @object["quoteURL"] && !@object["quoteURL"].empty?
@object["quoteURL"]
elsif @object["_misskey_quote"] && !@object["_misskey_quote"].empty?
@object["_misskey_quote"]
else
nil
end
end
def process_quote
url = guess_quote_url
return nil if url.nil?
quote = ResolveURLService.new.call(url)
status_from_uri(quote.uri) if quote
rescue
nil
end
end end

View File

@ -79,11 +79,15 @@ class ActivityPub::TagManager
# Unlisted and private statuses go out primarily to the followers collection # Unlisted and private statuses go out primarily to the followers collection
# Others go out only to the people they mention # Others go out only to the people they mention
def to(status) def to(status)
to = []
to << uri_for(status.quote.account) if status.quote?
case status.visibility case status.visibility
when 'public' when 'public'
[COLLECTIONS[:public]] to << COLLECTIONS[:public]
when 'unlisted', 'private' when 'unlisted', 'private'
[account_followers_url(status.account)] to << account_followers_url(status.account)
when 'direct', 'limited' when 'direct', 'limited'
if status.account.silenced? if status.account.silenced?
# Only notify followers if the account is locally silenced # Only notify followers if the account is locally silenced

View File

@ -4,7 +4,6 @@
# #
# Table name: accounts # Table name: accounts
# #
# id :bigint(8) not null, primary key
# username :string default(""), not null # username :string default(""), not null
# domain :string # domain :string
# private_key :text # private_key :text
@ -17,11 +16,11 @@
# url :string # url :string
# avatar_file_name :string # avatar_file_name :string
# avatar_content_type :string # avatar_content_type :string
# avatar_file_size :integer # avatar_file_size :bigint(8)
# avatar_updated_at :datetime # avatar_updated_at :datetime
# header_file_name :string # header_file_name :string
# header_content_type :string # header_content_type :string
# header_file_size :integer # header_file_size :bigint(8)
# header_updated_at :datetime # header_updated_at :datetime
# avatar_remote_url :string # avatar_remote_url :string
# locked :boolean default(FALSE), not null # locked :boolean default(FALSE), not null
@ -32,6 +31,7 @@
# shared_inbox_url :string default(""), not null # shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null # followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null # protocol :integer default("ostatus"), not null
# id :bigint(8) not null, primary key
# memorial :boolean default(FALSE), not null # memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8) # moved_to_account_id :bigint(8)
# featured_collection_url :string # featured_collection_url :string
@ -45,8 +45,8 @@
# avatar_storage_schema_version :integer # avatar_storage_schema_version :integer
# header_storage_schema_version :integer # header_storage_schema_version :integer
# devices_url :string # devices_url :string
# suspension_origin :integer
# sensitized_at :datetime # sensitized_at :datetime
# suspension_origin :integer
# trendable :boolean # trendable :boolean
# reviewed_at :datetime # reviewed_at :datetime
# requested_review_at :datetime # requested_review_at :datetime

View File

@ -9,7 +9,7 @@
# domain :string # domain :string
# image_file_name :string # image_file_name :string
# image_content_type :string # image_content_type :string
# image_file_size :integer # image_file_size :bigint(8)
# image_updated_at :datetime # image_updated_at :datetime
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null

View File

@ -4,16 +4,16 @@
# #
# Table name: imports # Table name: imports
# #
# id :bigint(8) not null, primary key
# type :integer not null # type :integer not null
# approved :boolean default(FALSE), not null # approved :boolean default(FALSE), not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# data_file_name :string # data_file_name :string
# data_content_type :string # data_content_type :string
# data_file_size :integer # data_file_size :bigint(8)
# data_updated_at :datetime # data_updated_at :datetime
# account_id :bigint(8) not null # account_id :bigint(8) not null
# id :bigint(8) not null, primary key
# overwrite :boolean default(FALSE), not null # overwrite :boolean default(FALSE), not null
# #

View File

@ -4,11 +4,10 @@
# #
# Table name: media_attachments # Table name: media_attachments
# #
# id :bigint(8) not null, primary key
# status_id :bigint(8) # status_id :bigint(8)
# file_file_name :string # file_file_name :string
# file_content_type :string # file_content_type :string
# file_file_size :integer # file_file_size :bigint(8)
# file_updated_at :datetime # file_updated_at :datetime
# remote_url :string default(""), not null # remote_url :string default(""), not null
# created_at :datetime not null # created_at :datetime not null
@ -17,6 +16,7 @@
# type :integer default("image"), not null # type :integer default("image"), not null
# file_meta :json # file_meta :json
# account_id :bigint(8) # account_id :bigint(8)
# id :bigint(8) not null, primary key
# description :text # description :text
# scheduled_status_id :bigint(8) # scheduled_status_id :bigint(8)
# blurhash :string # blurhash :string
@ -24,7 +24,7 @@
# file_storage_schema_version :integer # file_storage_schema_version :integer
# thumbnail_file_name :string # thumbnail_file_name :string
# thumbnail_content_type :string # thumbnail_content_type :string
# thumbnail_file_size :integer # thumbnail_file_size :bigint(8)
# thumbnail_updated_at :datetime # thumbnail_updated_at :datetime
# thumbnail_remote_url :string # thumbnail_remote_url :string
# #

View File

@ -10,7 +10,7 @@
# description :string default(""), not null # description :string default(""), not null
# image_file_name :string # image_file_name :string
# image_content_type :string # image_content_type :string
# image_file_size :integer # image_file_size :bigint(8)
# image_updated_at :datetime # image_updated_at :datetime
# type :integer default("link"), not null # type :integer default("link"), not null
# html :text default(""), not null # html :text default(""), not null

View File

@ -8,7 +8,7 @@
# var :string default(""), not null # var :string default(""), not null
# file_file_name :string # file_file_name :string
# file_content_type :string # file_content_type :string
# file_file_size :integer # file_file_size :bigint(8)
# file_updated_at :datetime # file_updated_at :datetime
# meta :json # meta :json
# created_at :datetime not null # created_at :datetime not null

View File

@ -29,6 +29,7 @@
# edited_at :datetime # edited_at :datetime
# trendable :boolean # trendable :boolean
# ordered_media_attachment_ids :bigint(8) is an Array # ordered_media_attachment_ids :bigint(8) is an Array
# quote_id :bigint(8)
# #
class Status < ApplicationRecord class Status < ApplicationRecord
@ -61,6 +62,7 @@ class Status < ApplicationRecord
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
belongs_to :quote, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, optional: true
has_many :favourites, inverse_of: :status, dependent: :destroy has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy has_many :bookmarks, inverse_of: :status, dependent: :destroy
@ -71,6 +73,7 @@ class Status < ApplicationRecord
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account' has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify has_many :media_attachments, dependent: :nullify
has_many :quoted, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
has_and_belongs_to_many :tags has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards has_and_belongs_to_many :preview_cards
@ -87,6 +90,7 @@ class Status < ApplicationRecord
validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :reblog, uniqueness: { scope: :account }, if: :reblog?
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true
validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
accepts_nested_attributes_for :poll accepts_nested_attributes_for :poll
@ -160,6 +164,17 @@ class Status < ApplicationRecord
account: [:account_stat, user: :role], account: [:account_stat, user: :role],
active_mentions: { account: :account_stat }, active_mentions: { account: :account_stat },
], ],
quote: [
:application,
:tags,
:preview_cards,
:media_attachments,
:conversation,
:status_stat,
:preloadable_poll,
account: [:account_stat, :user],
active_mentions: { account: :account_stat },
],
thread: { account: :account_stat } thread: { account: :account_stat }
delegate :domain, to: :account, prefix: true delegate :domain, to: :account, prefix: true
@ -225,6 +240,14 @@ class Status < ApplicationRecord
!reblog_of_id.nil? !reblog_of_id.nil?
end end
def quote?
!quote_id.nil? && quote
end
def quote_visibility
quote&.visibility
end
def within_realtime_window? def within_realtime_window?
created_at >= REAL_TIME_WINDOW.ago created_at >= REAL_TIME_WINDOW.ago
end end
@ -289,7 +312,7 @@ class Status < ApplicationRecord
fields = [spoiler_text, text] fields = [spoiler_text, text]
fields += preloadable_poll.options unless preloadable_poll.nil? fields += preloadable_poll.options unless preloadable_poll.nil?
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) @emojis = CustomEmoji.from_text(fields.join(' '), account.domain) + (quote? ? CustomEmoji.from_text([quote.spoiler_text, quote.text].join(' '), quote.account.domain) : [])
end end
def ordered_media_attachments def ordered_media_attachments

View File

@ -62,6 +62,10 @@ class StatusEdit < ApplicationRecord
end end
end end
def quote?
status.quote?
end
def proper def proper
self self
end end

View File

@ -3,7 +3,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer class ActivityPub::NoteSerializer < ActivityPub::Serializer
include FormattingHelper include FormattingHelper
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message, :quote_uri
attributes :id, :type, :summary, attributes :id, :type, :summary,
:in_reply_to, :published, :url, :in_reply_to, :published, :url,
@ -11,6 +11,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:atom_uri, :in_reply_to_atom_uri, :atom_uri, :in_reply_to_atom_uri,
:conversation :conversation
attribute :quote_uri, if: -> { object.quote? }
attribute :content attribute :content
attribute :content_map, if: :language? attribute :content_map, if: :language?
attribute :updated, if: :edited? attribute :updated, if: :edited?
@ -150,6 +152,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end end
end end
def quote_uri
ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
end
def local? def local?
object.account.local? object.account.local?
end end

View File

@ -186,3 +186,13 @@ class REST::StatusSerializer < ActiveModel::Serializer
end end
end end
end end
class REST::QuoteStatusSerializer < REST::StatusSerializer
attribute :quote do
nil
end
end
class REST::StatusSerializer < ActiveModel::Serializer
belongs_to :quote, serializer: REST::QuoteStatusSerializer
end

View File

@ -73,7 +73,7 @@ class FetchLinkCardService < BaseService
@status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize } @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
else else
document = Nokogiri::HTML(@status.text) document = Nokogiri::HTML(@status.text)
links = document.css('a') links = document.css(':not(.quote-inline) > a')
links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize) links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
end end

View File

@ -31,6 +31,7 @@ class PostStatusService < BaseService
# @option [String] :idempotency Optional idempotency key # @option [String] :idempotency Optional idempotency key
# @option [Boolean] :with_rate_limit # @option [Boolean] :with_rate_limit
# @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions # @option [Enumerable] :allowed_mentions Optional array of expected mentioned account IDs, raises `UnexpectedMentionsError` if unexpected accounts end up in mentions
# @option [String] :quote_id
# @return [Status] # @return [Status]
def call(account, options = {}) def call(account, options = {})
@account = account @account = account
@ -213,6 +214,7 @@ class PostStatusService < BaseService
application: @options[:application], application: @options[:application],
content_type: @options[:content_type] || @account.user&.setting_default_content_type, content_type: @options[:content_type] || @account.user&.setting_default_content_type,
rate_limit: @options[:with_rate_limit], rate_limit: @options[:with_rate_limit],
quote_id: @options[:quote_id],
}.compact }.compact
end end

View File

@ -19,7 +19,7 @@
- %w(57 60 72 76 114 120 144 152 167 180 1024).each do |size| - %w(57 60 72 76 114 120 144 152 167 180 1024).each do |size|
%link{ rel: 'apple-touch-icon', sizes: "#{size}x#{size}", href: asset_pack_path("media/icons/apple-touch-icon-#{size}x#{size}.png") }/ %link{ rel: 'apple-touch-icon', sizes: "#{size}x#{size}", href: asset_pack_path("media/icons/apple-touch-icon-#{size}x#{size}.png") }/
%link{ rel: 'mask-icon', href: asset_pack_path('media/images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'mask-icon', href: asset_pack_path('media/images/logo-symbol-icon.svg'), color: '#00AEEF' }/
%link{ rel: 'manifest', href: manifest_path(format: :json) }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/
%meta{ name: 'theme-color', content: '#191b22' }/ %meta{ name: 'theme-color', content: '#191b22' }/
%meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/ %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/

View File

@ -15,7 +15,11 @@
= account_action_button(status.account) = account_action_button(status.account)
- if status.quote?
= render partial: "statuses/quote_status", locals: {status: status.quote}
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text? - if status.spoiler_text?
%p< %p<
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp; %span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp;

View File

@ -0,0 +1,35 @@
.status.quote-status{ dataurl: ActivityPub::TagManager.instance.url_for(status) }
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do
.status__avatar
%div
= image_tag status.account.avatar_static_url, width: 18, height: 18, alt: '', class: 'u-photo account__avatar'
%span.display-name
%bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true)
&nbsp;
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
.status__content.emojify<
- if status.spoiler_text?
%p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
%span.p-summary> #{Formatter.instance.format_spoiler(status)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}" }
= Formatter.instance.format_in_quote(status, custom_emojify: true)
- if !status.media_attachments.empty?
- if status.media_attachments.first.video?
- video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description, quote: true do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.media_attachments.first.audio?
- audio = status.media_attachments.first
= react_component :audio, src: audio.file.url(:original), height: 60, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, quote: true do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.preview_card
= react_component :card, maxDescription: 10, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, quote: true

View File

@ -27,7 +27,12 @@
%span.display-name__account %span.display-name__account
= acct(status.account) = acct(status.account)
= fa_icon('lock') if status.account.locked? = fa_icon('lock') if status.account.locked?
- if status.quote?
= render partial: "statuses/quote_status", locals: {status: status.quote}
.status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text? - if status.spoiler_text?
%p< %p<
%span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp; %span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp;

View File

@ -103,7 +103,7 @@ en:
imports: imports:
data: CSV file exported from another Mastodon server data: CSV file exported from another Mastodon server
invite_request: invite_request:
text: This will help us review your application text: "Be sure to provide at least one link to an established account on another social website: GitHub, Twitter, or a personal blog or website. This will help us review your request in a timely manner."
ip_block: ip_block:
comment: Optional. Remember why you added this rule. comment: Optional. Remember why you added this rule.
expires_in: IP addresses are a finite resource, they are sometimes shared and often change hands. For this reason, indefinite IP blocks are not recommended. expires_in: IP addresses are a finite resource, they are sometimes shared and often change hands. For this reason, indefinite IP blocks are not recommended.

View File

@ -0,0 +1,5 @@
class AddQuoteIdToStatuses < ActiveRecord::Migration[6.1]
def change
add_column :statuses, :quote_id, :bigint, null: true, default: nil
end
end

View File

@ -0,0 +1,7 @@
class AddIndexToStatusesQuoteId < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :statuses, :quote_id, algorithm: :concurrently
end
end

View File

@ -153,11 +153,11 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.string "url" t.string "url"
t.string "avatar_file_name" t.string "avatar_file_name"
t.string "avatar_content_type" t.string "avatar_content_type"
t.integer "avatar_file_size" t.bigint "avatar_file_size"
t.datetime "avatar_updated_at" t.datetime "avatar_updated_at"
t.string "header_file_name" t.string "header_file_name"
t.string "header_content_type" t.string "header_content_type"
t.integer "header_file_size" t.bigint "header_file_size"
t.datetime "header_updated_at" t.datetime "header_updated_at"
t.string "avatar_remote_url" t.string "avatar_remote_url"
t.boolean "locked", default: false, null: false t.boolean "locked", default: false, null: false
@ -181,8 +181,8 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.integer "avatar_storage_schema_version" t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version" t.integer "header_storage_schema_version"
t.string "devices_url" t.string "devices_url"
t.integer "suspension_origin"
t.datetime "sensitized_at" t.datetime "sensitized_at"
t.integer "suspension_origin"
t.boolean "trendable" t.boolean "trendable"
t.datetime "reviewed_at" t.datetime "reviewed_at"
t.datetime "requested_review_at" t.datetime "requested_review_at"
@ -353,7 +353,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.string "domain" t.string "domain"
t.string "image_file_name" t.string "image_file_name"
t.string "image_content_type" t.string "image_content_type"
t.integer "image_file_size" t.bigint "image_file_size"
t.datetime "image_updated_at" t.datetime "image_updated_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -520,7 +520,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "data_file_name" t.string "data_file_name"
t.string "data_content_type" t.string "data_content_type"
t.integer "data_file_size" t.bigint "data_file_size"
t.datetime "data_updated_at" t.datetime "data_updated_at"
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.boolean "overwrite", default: false, null: false t.boolean "overwrite", default: false, null: false
@ -541,12 +541,12 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
end end
create_table "ip_blocks", force: :cascade do |t| create_table "ip_blocks", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "expires_at"
t.inet "ip", default: "0.0.0.0", null: false t.inet "ip", default: "0.0.0.0", null: false
t.integer "severity", default: 0, null: false t.integer "severity", default: 0, null: false
t.datetime "expires_at"
t.text "comment", default: "", null: false t.text "comment", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
end end
@ -596,7 +596,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.bigint "status_id" t.bigint "status_id"
t.string "file_file_name" t.string "file_file_name"
t.string "file_content_type" t.string "file_content_type"
t.integer "file_file_size" t.bigint "file_file_size"
t.datetime "file_updated_at" t.datetime "file_updated_at"
t.string "remote_url", default: "", null: false t.string "remote_url", default: "", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -612,7 +612,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.integer "file_storage_schema_version" t.integer "file_storage_schema_version"
t.string "thumbnail_file_name" t.string "thumbnail_file_name"
t.string "thumbnail_content_type" t.string "thumbnail_content_type"
t.integer "thumbnail_file_size" t.bigint "thumbnail_file_size"
t.datetime "thumbnail_updated_at" t.datetime "thumbnail_updated_at"
t.string "thumbnail_remote_url" t.string "thumbnail_remote_url"
t.index ["account_id", "status_id"], name: "index_media_attachments_on_account_id_and_status_id", order: { status_id: :desc } t.index ["account_id", "status_id"], name: "index_media_attachments_on_account_id_and_status_id", order: { status_id: :desc }
@ -779,7 +779,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.string "description", default: "", null: false t.string "description", default: "", null: false
t.string "image_file_name" t.string "image_file_name"
t.string "image_content_type" t.string "image_content_type"
t.integer "image_file_size" t.bigint "image_file_size"
t.datetime "image_updated_at" t.datetime "image_updated_at"
t.integer "type", default: 0, null: false t.integer "type", default: 0, null: false
t.text "html", default: "", null: false t.text "html", default: "", null: false
@ -890,7 +890,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.string "var", default: "", null: false t.string "var", default: "", null: false
t.string "file_file_name" t.string "file_file_name"
t.string "file_content_type" t.string "file_content_type"
t.integer "file_file_size" t.bigint "file_file_size"
t.datetime "file_updated_at" t.datetime "file_updated_at"
t.json "meta" t.json "meta"
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -918,8 +918,8 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
create_table "status_pins", force: :cascade do |t| create_table "status_pins", force: :cascade do |t|
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.bigint "status_id", null: false t.bigint "status_id", null: false
t.datetime "created_at", default: -> { "now()" }, null: false t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
t.datetime "updated_at", default: -> { "now()" }, null: false t.datetime "updated_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true
t.index ["status_id"], name: "index_status_pins_on_status_id" t.index ["status_id"], name: "index_status_pins_on_status_id"
end end
@ -970,6 +970,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.datetime "edited_at" t.datetime "edited_at"
t.boolean "trendable" t.boolean "trendable"
t.bigint "ordered_media_attachment_ids", array: true t.bigint "ordered_media_attachment_ids", array: true
t.bigint "quote_id"
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
t.index ["account_id"], name: "index_statuses_on_account_id" t.index ["account_id"], name: "index_statuses_on_account_id"
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)" t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
@ -977,6 +978,7 @@ ActiveRecord::Schema.define(version: 2023_03_30_155710) do
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))" t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)" t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
t.index ["quote_id"], name: "index_statuses_on_quote_id"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id" t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)" t.index ["uri"], name: "index_statuses_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
end end

View File

@ -2,16 +2,17 @@ version: '3'
services: services:
db: db:
restart: always restart: always
image: postgres:14-alpine image: postgres:15-alpine
shm_size: 256mb shm_size: 256mb
networks: networks:
- internal_network - internal_network
healthcheck: healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres'] test: ['CMD', 'pg_isready', '-U', 'postgres']
volumes: volumes:
- ./postgres14:/var/lib/postgresql/data - ./data/postgres.15:/var/lib/postgresql/data
environment: environment:
- 'POSTGRES_HOST_AUTH_METHOD=trust' - 'POSTGRES_HOST_AUTH_METHOD=trust'
- 'POSTGRES_USER=mastodon'
redis: redis:
restart: always restart: always
@ -21,7 +22,7 @@ services:
healthcheck: healthcheck:
test: ['CMD', 'redis-cli', 'ping'] test: ['CMD', 'redis-cli', 'ping']
volumes: volumes:
- ./redis:/data - ./data/redis:/data
# es: # es:
# restart: always # restart: always
@ -56,7 +57,7 @@ services:
web: web:
build: . build: .
image: ghcr.io/mastodon/mastodon image: gitea.treehouse.systems/treehouse/mastodon:latest
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@ -66,18 +67,29 @@ services:
healthcheck: healthcheck:
# prettier-ignore # prettier-ignore
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1'] test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
expose:
- 3000
ports: ports:
- '127.0.0.1:3000:3000' - 3000:3000
labels:
- traefik.enable=true
- traefik.http.routers.web.rule=Host(`social-dev.treehouse.systems`)
- traefik.http.routers.web.tls=true
- traefik.http.routers.web.tls.certresolver=le
- traefik.http.routers.web.tls.domains[0].main=social-dev.treehouse.systems
- traefik.http.routers.web.entrypoints=websecure
- traefik.http.services.web.loadbalancer.server.port=3000
depends_on: depends_on:
- db - db
- redis - redis
# - es # - es
volumes: volumes:
- ./public/system:/mastodon/public/system - ./public/system:/mastodon/public/system
# - ./data/postgres:/var/lib/postgresql/data
streaming: streaming:
build: . build: .
image: ghcr.io/mastodon/mastodon image: gitea.treehouse.systems/treehouse/mastodon:latest
restart: always restart: always
env_file: .env.production env_file: .env.production
command: node ./streaming command: node ./streaming
@ -87,15 +99,23 @@ services:
healthcheck: healthcheck:
# prettier-ignore # prettier-ignore
test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'] test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
ports: expose:
- '127.0.0.1:4000:4000' - 4000
labels:
- traefik.enable=true
- 'traefik.http.routers.streaming.rule=Host(`social-dev.treehouse.systems`) && PathPrefix(`/api/v1/streaming/`)'
- traefik.http.routers.streaming.tls=true
- traefik.http.routers.streaming.tls.certresolver=le
- traefik.http.routers.streaming.tls.domains[0].main=social-dev.treehouse.systems
- traefik.http.routers.streaming.entrypoints=websecure
- traefik.http.services.streaming.loadbalancer.server.port=4000
depends_on: depends_on:
- db - db
- redis - redis
sidekiq: sidekiq:
build: . build: .
image: ghcr.io/mastodon/mastodon image: gitea.treehouse.systems/treehouse/mastodon:latest
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq command: bundle exec sidekiq

1
emoji_data/all.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'pathname'
def setup_redis_env_url(prefix = nil, defaults = true) def setup_redis_env_url(prefix = nil, defaults = true)
prefix = "#{prefix.to_s.upcase}_" unless prefix.nil? prefix = "#{prefix.to_s.upcase}_" unless prefix.nil?
prefix = '' if prefix.nil? prefix = '' if prefix.nil?
redis_url_key = "#{prefix}REDIS_URL"
return if ENV["#{prefix}REDIS_URL"].present? if ENV[redis_url_key].present?
conn = +ENV["#{prefix}REDIS_URL"].sub(/redis:\/\//i, '')
# Strip any prefixing `unix://`
unix = !conn.sub!(/^unix:\/\//i, '').nil?
# Strip any prefixing `./`
unix |= conn.sub!(/^(\.\/)+/, '')
unix |= conn.start_with?('/')
if unix
pn = Pathname.new(conn)
pn = Pathname.getwd / pn if pn.relative?
ENV[redis_url_key] = "unix://#{pn}"
end
return
end
password = ENV.fetch("#{prefix}REDIS_PASSWORD") { '' if defaults } password = ENV.fetch("#{prefix}REDIS_PASSWORD") { '' if defaults }
host = ENV.fetch("#{prefix}REDIS_HOST") { 'localhost' if defaults } host = ENV.fetch("#{prefix}REDIS_HOST") { 'localhost' if defaults }

View File

@ -21,7 +21,7 @@ module Mastodon
end end
def suffix def suffix
"+glitch#{ENV.fetch('MASTODON_VERSION_SUFFIX', '')}" "+glitch#{ENV.fetch('MASTODON_VERSION_SUFFIX', '.th')}"
end end
def to_a def to_a
@ -33,21 +33,30 @@ module Mastodon
end end
def repository def repository
ENV.fetch('GITHUB_REPOSITORY', 'glitch-soc/mastodon') ENV.fetch('GIT_REPOSITORY', false) || ENV.fetch('GITHUB_REPOSITORY', false) || 'treehouse/mastodon'
end end
def source_base_url def source_base_url
ENV.fetch('SOURCE_BASE_URL', "https://github.com/#{repository}") base = ENV['GITHUB_REPOSITORY'] ? 'https://github.com' : 'https://gitea.treehouse.systems'
ENV.fetch('SOURCE_BASE_URL', "#{base}/#{repository}")
end end
# specify git tag or commit hash here # specify git tag or commit hash here
def source_tag def source_tag
ENV.fetch('SOURCE_TAG', nil) tag = ENV.fetch('SOURCE_TAG', nil)
return if tag.nil? || tag.empty?
tag
end end
def source_url def source_url
if source_tag tag = source_tag
"#{source_base_url}/tree/#{source_tag}" if tag && source_base_url =~ /gitea/
suffix = if !tag[/\H/]
"commit/#{tag}"
else
"branch/#{tag}"
end
"#{source_base_url}/#{suffix}"
else else
source_base_url source_base_url
end end

View File

@ -31,6 +31,7 @@ class Sanitize
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
next true if /^(mention|hashtag)$/.match?(e) # semantic classes next true if /^(mention|hashtag)$/.match?(e) # semantic classes
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
next true if /^quote-inline$/.match?(e) # quote inline classes
end end
node['class'] = class_list.join(' ') node['class'] = class_list.join(' ')

109
lib/tasks/deps.rake Normal file
View File

@ -0,0 +1,109 @@
# frozen_string_literal: true
require 'pathname'
DATA_DIR = Pathname.new('data')
POSTGRES_DIR = DATA_DIR / 'postgres'
POSTGRES_CONF_FILE = POSTGRES_DIR / 'postgresql.conf'
POSTGRES_SOCKET_FILE = POSTGRES_DIR / '.s.PGSQL.5432'
POSTGRES_PID_FILE = POSTGRES_DIR / 'postmaster.pid'
REDIS_DIR = DATA_DIR / 'redis'
REDIS_PID_FILE = REDIS_DIR / 'redis-dev.pid'
def divider
puts '=========='
end
def get_pid(pid_file)
return false unless File.file?(pid_file)
pid = File.read(pid_file).to_i
Process.kill(0, pid)
pid
rescue Errno::ESRCH
nil
end
def postgres_running?
get_pid POSTGRES_PID_FILE
end
directory REDIS_DIR.to_s
namespace :deps do
task start: ['postgres:start', 'redis:start']
task stop: ['postgres:stop', 'redis:stop']
namespace :postgres do
namespace :setup do
task all: [POSTGRES_DIR.to_s]
file POSTGRES_DIR.to_s do
if POSTGRES_CONF_FILE.exist?
puts 'Postgres conf exists, skipping initdb'
next
end
sh %(printf '%s\\n' pg_ctl -D data/postgres initdb -o '-U mastodon --auth-host=trust')
end
task configure: [POSTGRES_DIR.to_s] do
next if File.foreach(POSTGRES_CONF_FILE).detect? { |line| line == /^unix_socket_directories = \.\s*$/ }
POSTGRES_CONF_FILE.open('at') do |f|
f.write("\n", PG_SOCKET_DIRECTORIES_LINE, "\n")
end
end
end
task start: ['setup:all'] do
if (pid = get_pid POSTGRES_PID_FILE)
puts "Postgres is running (pid #{pid})!"
next
end
puts 'Starting postgres...'
divider
sh %(pg_ctl -D ./data/postgres start)
divider
end
task :stop do
unless (pid = get_pid POSTGRES_PID_FILE)
puts "Postgres isn't running!"
next
end
puts "Stopping Postgres (pid #{pid})..."
sh %(pg_ctl -D ./data/postgres stop)
divider
end
end
namespace :redis do
task init: [REDIS_DIR.to_s] do
end
task start: [:init] do
if (pid = get_pid REDIS_PID_FILE)
puts "Redis is running (pid #{pid})!"
next
end
puts 'Starting redis...'
divider
sh %(redis-server redis-dev.conf)
divider
end
task :stop do
unless (pid = get_pid REDIS_PID_FILE)
puts "Redis isn't running!"
next
end
puts "Stopping Redis (pid #{pid})..."
divider
Process.kill(:TERM, pid)
end
end
end

Some files were not shown because too many files have changed in this diff Show More