Compare commits

...

545 Commits

Author SHA1 Message Date
Blackle Mori 0f84a7174c Fix linear gradient colour on collapsed statuses in a few skins 2019-08-15 23:55:47 -05:00
multiple creatures f9eaffc790 relax the the media proxy rate limit when logged in (now with 300% more relaxation) 2019-08-15 23:47:23 -05:00
multiple creatures d2eb644d45 apply custom filters and cws to search + remove unused es search methods 2019-08-15 23:41:34 -05:00
multiple creatures f40c6dbc93 handle custom cws when a filter has the `entire thread` option set 2019-08-15 23:22:43 -05:00
multiple creatures 14cf223041 only drop posts matching a custom filter when the filter has no cw override set 2019-08-15 22:53:02 -05:00
multiple creatures 7bbcf793bc custom filters now have an option to add or override content warnings; filter caching has been fixed 2019-08-15 22:40:20 -05:00
multiple creatures f783ec279d allow out-of-body mentions 2019-08-15 17:07:59 -05:00
multiple creatures f54329f9d6 alias `quit` bangtag to `part` + `part:reply-guy-mode` egg 2019-08-15 17:07:56 -05:00
multiple creatures 7ce8751692 fix arachnia & seadrake media padding 2019-08-15 12:16:45 -05:00
multiple creatures e8a5c9a972 fix arachnia & seadrake colors 2019-08-15 12:15:23 -05:00
multiple creatures 18e727efdf don't bother searching if term is empty 2019-08-15 11:12:17 -05:00
multiple creatures bbb025be69 add ability to search own posts by prepending `me:` to searches 2019-08-15 11:09:36 -05:00
multiple creatures 5ae918d968 add cw to moderation bangtags if missing 2019-08-15 10:58:07 -05:00
multiple creatures ad81a82ea4 initial fixes of orctober colors 2019-08-15 10:38:58 -05:00
multiple creatures c8a9f38bcd initial fixes of tentacle colors 2019-08-15 10:25:07 -05:00
multiple creatures b4b6f39c87 fix padding on orctober & tentacle themes 2019-08-15 09:53:07 -05:00
multiple creatures 903cabc4b2 fix the header padding 2019-08-15 09:47:42 -05:00
multiple creatures cc3cf7b606 `target_account.user` -> `target_account` 2019-08-15 09:27:43 -05:00
multiple creatures e2e0eddda7 fix err 500 when someone attempts to follow someone they're already following 2019-08-15 09:27:43 -05:00
multiple creatures b6b5bae72c fix err 500 when trying to read `identity_proofs` from a non-existant account 2019-08-15 09:27:43 -05:00
Lumb c19fc4cf9b remove negative margins for direct messages and boosts in timeline
What the tin says, noticed this bug was still hanging around, just only on DMs and on boosts in the TL so I think this should sort that.
2019-08-15 09:27:43 -05:00
multiple creatures a06f8140d9 handle interactions on sharekeyed posts when both participants are local; allow faving sharekeyed posts 2019-08-15 09:27:43 -05:00
multiple creatures 6d026c5007 fix accidental rename 2019-08-15 09:27:43 -05:00
Lumb c9dc797ef2 remove negative margin
this should fix the names being cut off on the notification bar uwu uwu
2019-08-12 10:53:02 +00:00
multiple creatures 8a7605e502 fix interaction buttons on public pages 2019-08-10 03:21:04 -05:00
multiple creatures 2a41140dc7 ...and a different newline syntax too. 2019-08-10 01:43:01 -05:00
multiple creatures 1cdf37da83 Gitea uses a different Markdown quote syntax. 2019-08-10 01:41:44 -05:00
multiple creatures 0ea96118af Finally add a proper README. 2019-08-10 01:39:34 -05:00
multiple creatures eac4369868 all i am saaaaaaaaaying / is use up less raaaaaaam
This reverts commit ed9c8f67c4.
2019-08-09 21:25:51 -05:00
multiple creatures 5f08b96cbe broadcast monsterfork as such 2019-08-09 21:25:04 -05:00
multiple creatures b2d0389fea transparancy log - automatically add a content warning if there are admin comments; support a custom `subject` 2019-08-09 19:13:04 -05:00
multiple creatures 8c196e70b1 transparancy log - do not direct link to domains 2019-08-09 19:02:30 -05:00
multiple creatures e466f9c2ce janitor - add db & media pruning job 2019-08-09 19:00:58 -05:00
multiple creatures d8156acb06 update yarn.lock 2019-08-09 18:58:51 -05:00
multiple creatures a4b7b5c132 fedi privacy - reject incoming out-of-scope posts addressed to private/unresolvable accounts & not addressed to any local users 2019-08-08 20:09:21 -05:00
multiple creatures e496fd473f default to reject unknown policy for new servers 2019-08-08 12:46:28 -05:00
multiple creatures ecd461aa78 admin transparancy log - add moderator review links to domain policy changes 2019-08-08 12:46:23 -05:00
multiple creatures 4283f15493 admin transparancy log - support markdown 2019-08-08 12:46:23 -05:00
multiple creatures 4dfc40324b add new `reject unknown` policy option to prevent spam & harassment from large/undermoderated servers 2019-08-08 12:46:17 -05:00
multiple creatures d019e55b7b janitor - option to export `activityrelay` config 2019-08-08 09:37:34 -05:00
multiple creatures d8a1e472c2 janitor - make sure values excludes are deduped 2019-08-08 09:36:05 -05:00
multiple creatures bcfa50f5f5 unapply `force_sensitive` during domain policy resets 2019-08-07 19:05:48 -05:00
multiple creatures 72592b3c9c add distributability check to reblog worker 2019-08-07 01:51:02 -05:00
multiple creatures ef04f3879a add option to automatically space out boosts over configurable random intervals 2019-08-07 01:08:34 -05:00
multiple creatures a8475313b8 add ability to post as linked account without switching using `once:i:am`/`once:we:are` bangtag 2019-08-07 01:08:34 -05:00
multiple creatures d9a8c50f92 remember login & clear signed-in notice when switching accounts 2019-08-07 01:08:31 -05:00
multiple creatures ff22f11aae `@options[:delayed]` -> `@options[:delayed].present?` 2019-08-06 22:23:57 -05:00
multiple creatures 2329043e7b do not parse blocks from `dialup.express` or `tenforward.social` - too much margin-of-error without a machine-readable list 2019-08-06 22:00:53 -05:00
multiple creatures b564aac6f3 `account:` output posts auto-expire after 1 hour 2019-08-06 14:19:04 -05:00
multiple creatures da389a664b added ability to link accounts with `account🔗token` + `account🔗add` & switch between them with `i:am`/`we:are` bangtags; remove links with `account🔗del:USERNAME` or `account🔗clear`; list links with `account🔗list` 2019-08-06 13:55:54 -05:00
multiple creatures 647ac0f86a `unsuspend` -> `"unsuspend"` 2019-08-05 21:30:34 -05:00
multiple creatures 5e3ab78fa4 add config + utilities + instructions for `nginx`-level domain blocks 2019-08-05 19:47:30 -05:00
multiple creatures 2ff40d3788 janitor can now write suspended domains to whatever JANITOR_BLOCKLIST_OUTPUT is set to so they can be imported elsewhere (e.g., `nginx`) 2019-08-05 17:04:42 -05:00
multiple creatures 39a58f4061 ignore misc directory 2019-08-05 16:56:09 -05:00
multiple creatures d9cb0d32ed update schema 2019-08-05 01:54:08 -05:00
multiple creatures 1cd9fea3b5 add ability to access bookmarks from #self.bookmarks tag 2019-08-05 01:53:59 -05:00
multiple creatures 879166633c rewrite repeated domain maps to use a helper function & make vulpine.club yaml url a variable 2019-08-05 00:00:31 -05:00
multiple creatures f86a3314f7 port @zac@computerfox.xyz's `silence` to `BlocklistHelper` 2019-08-04 23:48:59 -05:00
multiple creatures 9a3c4bc051 restrict private pin visibility to packmates & do not include them in `featured` collection (mainline masto does not respect pin visibility) 2019-08-04 20:53:20 -05:00
multiple creatures 9ba2081720 allow more media proxy requests when logged in 2019-08-04 02:24:03 -05:00
multiple creatures 4e3d546f61 update `rack_attack` config from `glitch-soc` 2019-08-04 02:05:27 -05:00
multiple creatures f46293f6d9 limit inferred reject replies trigger to the start of first/last line; simplify text before matching 2019-08-04 00:26:33 -05:00
multiple creatures 2be5c8a55c automatically set `suspend` policy on domains that trigger `context` auto-rejections 2019-08-03 23:30:50 -05:00
multiple creatures 4801d5ac84 move `Scheduler::JanitorScheduler::admin_account` to `ModerationHelper::janitor_account` 2019-08-03 23:25:37 -05:00
multiple creatures 0a646efd48 make sure only thread authors and admins can close threads 2019-08-03 21:57:56 -05:00
multiple creatures d69ee097dd add `parent:bookmark`/`parent:bm` bangtag 2019-08-03 21:56:47 -05:00
multiple creatures 4427480356 `domain_exists` -> `domain_exists?` 2019-08-03 21:47:46 -05:00
multiple creatures 53a1f9d634 `reason?` -> `reason` 2019-08-03 21:01:16 -05:00
multiple creatures 3e6831d7d6 include note for admin account actions in audit log 2019-08-03 15:07:19 -05:00
multiple creatures e1bdc82d07 match more `reject_replies` tokens 2019-08-03 15:05:12 -05:00
multiple creatures 1785c93da7 add `noreplies`, `parent:close`, `thread:close` bangtags 2019-08-03 13:58:03 -05:00
multiple creatures b644f1c505 respect 'don't @ me' requests 2019-08-03 13:47:20 -05:00
multiple creatures cd52f75006 `domain_exists` -> `domain_exists?` 2019-08-03 06:31:55 -05:00
multiple creatures a2b9ac9a48 rename `BangtagHelper` to `ModerationHelper` 2019-08-03 06:30:54 -05:00
multiple creatures 60179e53ea skip importing dead domains 2019-08-03 06:28:25 -05:00
multiple creatures ceaf900dfc properly interpret `severity: nomedia` from `vulpine.club` yaml 2019-08-03 05:50:15 -05:00
multiple creatures a96d89ac56 fix 6 am typo - `+=` -> `=` 2019-08-03 05:41:38 -05:00
multiple creatures 6613005ae6 `monsterpit-janitor` is now built in as a sidekiq job (with better code) 2019-08-03 05:32:49 -05:00
multiple creatures 99d1b1ff6f fix web app crash when logging into a fresh account 2019-08-03 02:13:28 -05:00
multiple creatures f0094fd143 allow abandoning any thread; add `thread:leave`/`thread:part` bangtags 2019-08-02 20:55:21 -05:00
multiple creatures cf333d3699 unbreak - `tags_regex` -> `regex` 2019-08-02 20:12:59 -05:00
multiple creatures fc2e81c93f only domain policies have an `updated_at` attribute 2019-08-02 19:09:38 -05:00
multiple creatures dbcc560826 make `thread:emoji` actually grab all emojis in a thread 2019-08-02 15:04:41 -05:00
multiple creatures b1d125d704 unbreak the universe - `Redis.cache` -> `Redis.current` 2019-08-02 03:36:13 -05:00
multiple creatures e11196775f make sure the thread filter option won't affect your own threads or those you're participating in 2019-08-02 03:01:08 -05:00
multiple creatures 7cfc0f0dce fix name of `spoiler` column in migration 2019-08-02 02:32:22 -05:00
multiple creatures 65c42e5398 filters now have options to separately match post text, content warnings/titles, & hashtags + option to filter threads containing a matching post 2019-08-02 02:30:35 -05:00
multiple creatures 3813810cac bbcode: `[code]` produces `pre code` for `glitch-soc` compatability 2019-08-02 00:27:44 -05:00
multiple creatures 06fb561bd6 html5 has 2 strikethrough tags - support them both 2019-08-02 00:24:10 -05:00
multiple creatures a6e34404a2 strip params 2019-08-01 23:25:19 -05:00
multiple creatures 0e87431d61 when applying user filters make sure last check always returns a boolean 2019-08-01 22:34:56 -05:00
multiple creatures c4005a0b25 fix tag extraction failing on text in frozen state 2019-08-01 14:56:49 -05:00
multiple creatures 720207cf4b fix borkage caused by query string sanitization sometimes returning `nil` 2019-08-01 14:08:29 -05:00
multiple creatures c4bf59ed9c fix borkage caused by trying to concatinate `nil` values 2019-08-01 14:04:20 -05:00
multiple creatures 6bc75ce03b merge admin/moderator actions back under admin.log scope 2019-07-31 12:17:08 -05:00
multiple creatures 80a81fe223 ability to add domain moderation notes, edit existing domain policies in-place, and process asynchronously 2019-07-31 01:25:10 -05:00
multiple creatures 964054b6db sort instance list by most recent 2019-07-31 00:10:02 -05:00
multiple creatures 9e9a593f5a make `updated_at` attribute accessible in the instance model 2019-07-31 00:09:57 -05:00
multiple creatures a5ce8eddb4 use a non-tabular domain policy list layout & paginate 2019-07-30 20:24:37 -05:00
multiple creatures 85aec06845 order policy list by most recently changed 2019-07-30 18:39:26 -05:00
multiple creatures ccb84572d6 add a domain policy viewer at `/policies` 2019-07-30 16:36:24 -05:00
multiple creatures 3f327a3ea7 make cursor blink respect auto-play settings 2019-07-30 14:52:55 -05:00
multiple creatures 2ca0b8ce62 add missing `boolean_cast_setting` to `show_cursor` & `delayed_roars` preferences 2019-07-30 14:41:49 -05:00
multiple creatures 96770151ef remove auto-rejections from transparancy log 2019-07-30 13:54:13 -05:00
multiple creatures d51a846d3a revert to `text/plain` for transparancy logger 2019-07-30 13:37:33 -05:00
multiple creatures d9758157b9 add an optional blinking cursor to console formatting 2019-07-30 13:10:35 -05:00
multiple creatures 90130014dd add plain-text console formatting option 2019-07-29 23:09:51 -05:00
multiple creatures 0fb1e7888e actually let's make the delay time configurable 2019-07-29 18:17:51 -05:00
multiple creatures 9d55cfc6ad fix delayed roars mentions bug, increase delay to 1 minute, include local only prop 2019-07-29 17:40:44 -05:00
multiple creatures 6fa955e8a1 disable clear button when there's nothing to clear 2019-07-29 15:30:33 -05:00
multiple creatures 42bf20d22f too few args <.<' 2019-07-29 15:11:25 -05:00
multiple creatures 896817421a too many args (too many args) 2019-07-29 15:07:50 -05:00
multiple creatures 74290d4eb3 optional delayed publishing of roars for proofreading 2019-07-29 14:26:41 -05:00
multiple creatures 02729ab3ae update schema 2019-07-29 13:18:51 -05:00
multiple creatures feeb789ecd add `boostable` attribute to statuses model 2019-07-29 12:38:12 -05:00
multiple creatures 863c101e0a move clear button text to mouseover 2019-07-29 12:35:23 -05:00
multiple creatures 90d72f19ba big tails never fail! 2019-07-28 22:39:54 -05:00
multiple creatures 965b713ac2 move clear all button to the left 2019-07-28 21:05:00 -05:00
multiple creatures 4fc97d77a9 add clear button & missing monsterpit visibility icons 2019-07-28 20:58:40 -05:00
multiple creatures b93bf4b271 use identity nickname in composer placeholder instead (custom emoji won't work in placeholder text) 2019-07-28 19:33:31 -05:00
multiple creatures 30d3b9a6f7 add `i:am` shortname to the client api 2019-07-28 19:29:24 -05:00
multiple creatures 9e841ece20 make `i:am`/`we:are` signatures available to the client api 2019-07-28 19:06:02 -05:00
multiple creatures 28b2a700f0 show which identity roars are being signed with in the composer placeholder 2019-07-28 19:03:42 -05:00
multiple creatures 0f18a0ad00 fix `i:am:not`/`we:are:not` 2019-07-28 14:47:14 -05:00
multiple creatures c4f5de4e06 fix `adult content` setting & remove old `supports chat` setting 2019-07-28 14:45:43 -05:00
multiple creatures 712137fda9 make `i:am`/`we:are` handle multiple identities (`we:are:a:and:b:and:c`) and one-time authorship (`once:i:am:a`) 2019-07-26 19:46:14 -05:00
multiple creatures 31d2b16e43 add `once`/`once:` flag bangtag 2019-07-26 19:44:30 -05:00
multiple creatures 9febf12029 fix `delete_in:thread` logic flub (electric boogaloo) 2019-07-26 19:43:47 -05:00
multiple creatures aaa207284a make sure self-destructing roar worker exits successfully 2019-07-26 18:01:24 -05:00
multiple creatures cfb28743fa fix `delete_in:thread` logic flub 2019-07-26 18:00:49 -05:00
multiple creatures 1aba334730 process self-destructing roars asynchronously 2019-07-26 17:49:32 -05:00
multiple creatures f9e382b9a6 add `delete_in:thread`/`thread:delete_in` 2019-07-26 15:47:46 -05:00
multiple creatures 78dd3d0e92 fix display of report ids 2019-07-26 15:20:57 -05:00
multiple creatures 0151f14dbc split federation & moderation logs into different subscopes + make scope tag prefix configurable 2019-07-26 15:20:57 -05:00
multiple creatures e0b257d512 remove hashtag visibility warning - this fork supports unlisted hashtags 2019-07-26 15:20:57 -05:00
multiple creatures 5c27502afa bump poll sizes to 33 options x 202 chars 2019-07-26 17:30:16 +00:00
multiple creatures 0d17c2bf2e add all `ActionLog`-able admin & moderator actions to logger 2019-07-25 01:35:49 -05:00
multiple creatures 234fae09ad don't include `reject_payload!` auto-rejections in logger because that leaks objects sent to the server that weren't supposed to be 2019-07-24 21:36:13 -05:00
multiple creatures bf27f256c5 clarify rejected announces correctly (but even more correctly with the right json object) 2019-07-24 20:24:52 -05:00
multiple creatures cf28bbd9fa clarify rejected announces correctly 2019-07-24 20:00:04 -05:00
multiple creatures ab43f884e3 don't append account uri 2019-07-24 19:43:42 -05:00
multiple creatures 8945a3b534 moderation logger - add auto-reject reasons 2019-07-24 19:37:00 -05:00
multiple creatures 5ec6e9c1e2 tag `admin:` command usage as `monsterpit.admin.log` 2019-07-24 18:33:09 -05:00
multiple creatures c0d23aa032 add icons to auto-reject logs 2019-07-24 18:27:54 -05:00
multiple creatures 88c23ae912 logger should not generate link cards or mentions for entries 2019-07-24 18:01:00 -05:00
multiple creatures 9cd09d4a70 manually patch `ActivityPub::TagManager` back to version this codebase should be using 2019-07-24 17:44:19 -05:00
multiple creatures 1f7a5bb57e remove chat tables & adjust status table index 2019-07-24 16:40:58 -05:00
multiple creatures cefcad1130 transparancy - log use of admin related method calls & activitypub auto-rejections to a logger account 2019-07-24 16:39:58 -05:00
multiple creatures 8f6e737f38 remove instance actor code from tag manager for now 2019-07-24 14:01:54 -05:00
multiple creatures b75f7be799 use numeric announcer id 2019-07-24 13:48:06 -05:00
multiple creatures 4415e8b047 success message when processing bangtags-only scripts 2019-07-24 13:03:51 -05:00
multiple creatures 25d628fca3 revert the current unfinished chat implementation 2019-07-24 13:01:12 -05:00
multiple creatures d83fcfd1f1 simplify bbcode url regex 2019-07-24 12:18:26 -05:00
multiple creatures aaae5aee52 should not reject imported posts 2019-07-23 19:18:52 -05:00
multiple creatures de542eca57 clear caches after changing lifespan of existing roars 2019-07-23 18:20:34 -05:00
multiple creatures 913ef775ab add missing `delete_in` aliases 2019-07-23 18:16:05 -05:00
multiple creatures a73ec02673 set visibility of `admin` bangtags to local & tag with `#monsterpit.admin` 2019-07-23 17:28:39 -05:00
multiple creatures 3862f48c34 add self-destructing roars & `live`/`lifespan` bangtags 2019-07-23 16:48:08 -05:00
multiple creatures 2a6ccce070 fix `keysmash` - join chunks 2019-07-23 16:44:12 -05:00
multiple creatures d377c828ef fix typo in bangtag aliases parsing 2019-07-23 16:43:25 -05:00
multiple creatures 4836e1f5df log rejected uris 2019-07-23 00:17:10 -05:00
multiple creatures 6a2b323006 fix username matching 2019-07-22 22:52:28 -05:00
multiple creatures 7df4d0e132 add autoreject by ap username, grabbing from actor uri if no `preferredUsername` key 2019-07-22 22:49:07 -05:00
multiple creatures 54bc08a8a3 fix typo - `url` -> `@url` 2019-07-22 21:42:34 -05:00
multiple creatures c2e47f5871 autoreject check before fetching link preview cards & feeds 2019-07-22 21:37:11 -05:00
multiple creatures 2822fbc443 move autoreject check to own module & check for reject before pulling resources 2019-07-22 21:12:54 -05:00
multiple creatures 86f29a68fb allow autorejecting incoming ap activities by `id`, `@context`, and domain + autoject suspended domains & their subdomains 2019-07-22 20:14:29 -05:00
multiple creatures d82d7e0b2b anchor tagger - filenames must have a dot 2019-07-22 17:25:20 -05:00
multiple creatures 155c324a7b formatting examples in preferences 2019-07-22 12:54:35 -05:00
multiple creatures e14d543edd handle more edge cases in archor tagger 2019-07-22 11:41:02 -05:00
multiple creatures e3ecc0871c begone extra parenthesis 2019-07-21 22:20:24 -05:00
multiple creatures b0eade5ad6 allow self & signed-in local followers to read outbox when `hide public ap outbox` is set 2019-07-21 22:18:02 -05:00
multiple creatures acc1fb81fe allow self & signed-in local followers to see account when `hide public profile` is set 2019-07-21 22:17:58 -05:00
multiple creatures 47d9a34401 return 404 if `hide public profile` set 2019-07-21 21:06:22 -05:00
multiple creatures 084b950401 split `hide public profile` & `hide public ap outbox` into separate user options, make original `Account.hidden` prop federation-affecting `invisible mode` 2019-07-21 20:50:30 -05:00
multiple creatures bca5a3073f privacy - add option to disable public activitypub outbox 2019-07-21 20:21:20 -05:00
multiple creatures d9073f132b add more options for time range of roars visible to anonymous public profile viewers 2019-07-21 19:51:07 -05:00
multiple creatures 61461a5323 privacy - limit public profiles & public ap outboxes to last 6 days of history 2019-07-21 17:16:18 -05:00
multiple creatures 3582566a52 privacy - remove rss endpoint from account controller 2019-07-21 16:52:09 -05:00
multiple creatures 6de7b8e021 we don't need `invalidate_association_caches!` 2019-07-21 13:43:03 -05:00
multiple creatures 1cf4d5a83d make anchor tagger strip trailing punctuation from link text before matching 2019-07-20 23:26:01 -05:00
multiple creatures c4600411f7 fix anchor tagger filename matching 2019-07-20 23:25:46 -05:00
multiple creatures 19fc6952b2 stop putting boosts in the local timeline when the booster's visibility is `local` 2019-07-20 22:48:17 -05:00
multiple creatures 70080ce6e6 add `tf:stripachors` & `tf:striplinks` bangtags 2019-07-20 22:42:23 -05:00
multiple creatures c4718cd2be add `tf:head` bangtag 2019-07-20 22:32:14 -05:00
multiple creatures 7a37731210 make anchor tagger tag filename links as such 2019-07-20 21:59:38 -05:00
multiple creatures 0dabcbfb02 limit post search to own social graph 2019-07-20 21:06:49 -05:00
multiple creatures 483f550f9c fix query string sanitizer - use `query_values=` from `Addressable::URI` instead of `to_query` 2019-07-20 12:20:43 -05:00
multiple creatures 1edc2f1aeb fix display of `admin:eval` comments 2019-07-20 11:51:25 -05:00
multiple creatures f0506110c4 rewrite `admin:` bangtags to use moderation functions from bangtag helper module 2019-07-20 10:34:26 -05:00
multiple creatures 4cfff5b001 create bangtag helper module, add functions for transparent moderation in `admin:` commands 2019-07-20 10:33:50 -05:00
multiple creatures ed50fee09f replace output newlines with html breaks in `admin:eval` 2019-07-20 09:41:28 -05:00
multiple creatures 3ff2871b27 add newlines before signature & tags for vanilladon compat 2019-07-20 09:33:32 -05:00
multiple creatures 243cbb2861 fix typo in account model - `frozen` -> `froze` 2019-07-20 08:17:16 -05:00
multiple creatures c864465e71 re-cache post when changed by `visibility:parent` or `parent:tag`/`parent:untag` bangtags 2019-07-20 06:47:26 -05:00
multiple creatures 74e81d4ef7 `visibility` bangtag accepts additional `federate`/`nofederate` (`f`/`nf`) arg to set whether post can federate + add more aliases 2019-07-20 06:40:31 -05:00
multiple creatures be251c1eb4 properly distribute announcement posts made thru `admin:announce` 2019-07-20 06:39:20 -05:00
multiple creatures 2d99300a6d add `visibility:community`, `visibility:c` aliases 2019-07-20 06:37:20 -05:00
multiple creatures 29cdfc36fc fix adult content badge on public profiles 2019-07-19 20:44:57 -05:00
multiple creatures d9d2c9a77e removed upstream locked icon from ui 2019-07-19 19:53:28 -05:00
multiple creatures 4e28528888 make locked badge stand out more 2019-07-19 19:48:15 -05:00
multiple creatures 07794055f9 move admin/mod badges to top 2019-07-19 19:45:05 -05:00
multiple creatures 348dd5aa35 always show out-of-body tags for better accessibility 2019-07-19 19:31:19 -05:00
multiple creatures 2f8ac8838d revise tag conversion 2019-07-19 19:30:59 -05:00
multiple creatures 9b7e4018b0 don't strip valueless query strings 2019-07-19 18:22:58 -05:00
multiple creatures dc32d286bd use high-vis privacy icons by default 2019-07-19 17:04:54 -05:00
multiple creatures 6d07ba50f3 keep `locked` badge for approves-followers + add `frozen` badge for admin-locked accts, also federate `frozen` state 2019-07-19 17:03:15 -05:00
multiple creatures 3fda862ea0 add badge to ui for locked accounts 2019-07-19 14:53:13 -05:00
multiple creatures bc22ab034b move query string sanitizer to its own module & sanitize link cards as well 2019-07-19 10:16:33 -05:00
multiple creatures 23c36c2d7c make anchor tagging check full path 2019-07-19 09:21:45 -05:00
multiple creatures ff75f5ea4b update schema 2019-07-19 09:20:24 -05:00
multiple creatures cfd314432d correct references in kobold migration 2019-07-19 08:15:22 -05:00
multiple creatures 7c60955f06 make community visibility default for new accounts 2019-07-19 07:33:26 -05:00
multiple creatures 44e204613d remove aliases of old of kobold prefs from account model 2019-07-19 07:30:06 -05:00
multiple creatures e80921bf83 remove old version of kobold prefs from strings 2019-07-19 07:28:52 -05:00
multiple creatures 6578d02a0a remove old version of kobold prefs from ui 2019-07-19 07:28:36 -05:00
multiple creatures 66286178ad migrate the kobolds from user setting to account 2019-07-19 07:26:47 -05:00
multiple creatures 40debd9f80 federate account locked status (`mp:locked`) 2019-07-18 21:32:40 -05:00
multiple creatures 879a4a8029 unbreak logic 2019-07-18 21:13:12 -05:00
multiple creatures d620d1749d exclude mentions & hashtags from anchor tagging 2019-07-18 21:10:38 -05:00
multiple creatures d219ecded6 add icons to user-specified link text & potentially misleading links 2019-07-18 20:59:12 -05:00
multiple creatures b233e1eddf strip known tracking parameters (e.g., utm codes) from links 2019-07-18 16:12:41 -05:00
multiple creatures afa8bb3892 add missing visibility strings to preferences 2019-07-18 13:28:04 -05:00
multiple creatures 24c40ef9b9 add community visibility to the web app 2019-07-18 13:24:31 -05:00
multiple creatures 881ccb2de1 Improve the anti-spam prompt. 2019-07-18 12:40:38 -05:00
multiple creatures 42618190b1 add `visibility:parent` bangtag to allow retroactive post visibility changes (currently only between `local` & `unlisted`) 2019-07-18 12:08:17 -05:00
multiple creatures 96050ff1d9 alias `parent:permalink` to `parent:link` 2019-07-18 12:07:18 -05:00
multiple creatures 7f19514527 better handling of bangtag-only posts that produce no output 2019-07-18 12:06:45 -05:00
multiple creatures b28fae301a make sure announcements get streamed to the local tl & make them unlisted-local visibility 2019-07-17 20:05:22 -05:00
multiple creatures f927cb47b4 add `admin:announce` bangtag 2019-07-17 19:45:46 -05:00
multiple creatures 40d4eccb00 allow escaping bangtags with `#!!` 2019-07-17 18:11:18 -05:00
multiple creatures ddd84a97ad fix bbcode bracket workaround 2019-07-17 17:42:40 -05:00
multiple creatures 54c3ac4aba don't override mobile ui or drawer when resizing 2019-07-17 17:39:33 -05:00
multiple creatures 3f1e5d2f87 include skipped in `admin:` output 2019-07-17 17:29:04 -05:00
multiple creatures 6bffa56473 add `parent:urls` & `parent:domains` bangtags 2019-07-17 16:51:26 -05:00
multiple creatures 65b79ae188 allow admins to retag parent posts (`parent:tag:monsterpit.dev.todo`) 2019-07-17 16:00:28 -05:00
multiple creatures 83cb62809b pretty output for `admin:` bangtags 2019-07-17 15:55:40 -05:00
multiple creatures 9f2d158864 add `admin:eval` bangtag & make `admin:` output local-only 2019-07-17 15:54:57 -05:00
multiple creatures 6a5b0b65bb make `draft`s local-only 2019-07-17 15:53:32 -05:00
multiple creatures 6cb00bc91d pretty-format `i:am:list` & make output local-only 2019-07-17 15:52:24 -05:00
multiple creatures d3357a90fe use private user var for `media:desc` bangtag 2019-07-17 15:51:11 -05:00
Lumb 0189e487f8 Document search.sql
Added install instructions for full text search.
2019-07-17 02:22:47 -05:00
Lumb 5f03b404c4 Add dist/search.sql
Adding SQL file to set up full text search.
2019-07-17 02:22:47 -05:00
Lumb 7629b2c22b Call resize css
This should be the right place to add an import so it affects the whole site I think? I'm new to this, forgive.
2019-07-17 02:22:16 -05:00
Lumb 2e4bc1a64d Add css to auto-resize columns
I'm not totally confident in all this CSS but it does what it's supposed to. Auto-resizing columns, which makes the interface slightly more usable for me.
2019-07-17 02:21:59 -05:00
Lumb 6d8357a6f0 reset toot limit and pinned limit to merge upstream
What the desc says uwu
2019-07-17 07:08:14 +00:00
Lumb 05980a56d2 update size limits in env sample
Updated video and image size limit var to reflect monsterpits implementation, added gif size limit var
2019-07-17 07:08:14 +00:00
Lumb 2597f31daf add size var to controller file
Add MAX_SIZE_LIMIT to control image and video size limit to read from env file
2019-07-17 07:08:14 +00:00
Lumb 6181c72ff5 add search var to sample env
Adding MAX_SEARCH RESULTS to the env sample file, and documented
2019-07-17 07:08:14 +00:00
Lumb b052644d2e add search env to controller file
Add variable to controller file for search results
2019-07-17 07:08:14 +00:00
Lumb 5a93e171db add vars to sample env
Adding vars to the sample env for profile fields, display name characters, max image and video size, and max audio clip length
2019-07-17 07:08:14 +00:00
Lumb 4695ab5db9 Update '.env.production.sample'
Changed max toot char to 5000
Changed max pinned toots to 10
2019-07-17 07:08:14 +00:00
multiple creatures 1049c858ac make admin bangtag output use html 2019-07-17 02:00:34 -05:00
multiple creatures 12d5f1edb6 set correct content type for `draft`s 2019-07-17 01:59:40 -05:00
multiple creatures c135018d9f add `i:am:list` bangtag 2019-07-17 01:58:59 -05:00
multiple creatures efcd176d58 don't include parent components of scoped tags in folded tag list 2019-07-17 01:34:46 -05:00
multiple creatures 5e3a120120 fix parsing of tags for folding 2019-07-17 01:34:42 -05:00
multiple creatures 38a3c2b7b9 add `admin:unsuspend` bangtag & aliases 2019-07-17 01:14:08 -05:00
multiple creatures 92406964f1 fix `i:am`/`we:are` signatures 2019-07-17 00:32:56 -05:00
multiple creatures 2089a78f82 `admin:silence` and `admin:suspend` bangtags 2019-07-17 00:28:48 -05:00
dependabot-preview[bot] 17a701b443 [Security] Bump lodash from 4.17.11 to 4.17.13 (#11287)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.13. **This update includes security fixes.**
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.13)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-16 10:44:00 -05:00
multiple creatures c3127be31e add visibility shortcodes 2019-07-15 15:08:43 -05:00
multiple creatures 6daeca8a09 bangtags are not for eating >:o 2019-07-15 15:00:45 -05:00
multiple creatures 1c10ce6269 alias `visibility` bangtag to `v` 2019-07-15 14:51:55 -05:00
multiple creatures 0eeb7fa881 stop breaking things with missing commas 2019-07-15 14:44:54 -05:00
multiple creatures 6f1a07945e make sure user exists before migrating 2019-07-15 14:44:36 -05:00
multiple creatures 037be68060 ignore `tsv` column & add index to local-scope posts 2019-07-15 14:12:24 -05:00
multiple creatures 09eb7fb78d don't put boosts of silenced folks into home feed if you don't follow them 2019-07-15 14:12:24 -05:00
multiple creatures 4f3618f7be drop home feed & boost dedup window sizes to save resources 2019-07-15 14:12:24 -05:00
multiple creatures 2bbc06de5c show formatting of replies in composer area 2019-07-15 14:12:24 -05:00
multiple creatures 31a7ca0468 unbreak `domain_block_impact` 2019-07-15 14:12:24 -05:00
multiple creatures b441174bd2 drop incoming twitter retweets 2019-07-15 14:12:24 -05:00
multiple creatures 6b72e8a4df add text representation of statuses to status serializer 2019-07-15 14:12:24 -05:00
multiple creatures 07013fba48 revise status filters & make `hide mentions of blocked` also filter mentions of users blocked at the admin level 2019-07-15 14:12:24 -05:00
multiple creatures ab132569d7 only tolerate 3 days of remote failures before giving up 2019-07-15 14:12:24 -05:00
multiple creatures 7147447254 add `untag` bangtag 2019-07-15 14:12:24 -05:00
multiple creatures 27d67e2d5c sanitize bio formatting! 2019-07-15 14:12:24 -05:00
multiple creatures 441bead7ba workaround bbcode parser voring brackets 2019-07-15 14:12:24 -05:00
multiple creatures 45408c3c01 revise bbcode markup 2019-07-15 14:12:24 -05:00
multiple creatures 436f7984d9 move user variables to `users` relation 2019-07-15 14:12:24 -05:00
multiple creatures 9a2f0131c6 tag folding 2019-07-15 14:12:24 -05:00
multiple creatures 5e3ea221a8 add `supports_chat` property, rename `adults_only` to `adult_content`, federate the kobolds~ 2019-07-15 14:12:24 -05:00
multiple creatures cf3ec71aa5 local visibility scope, chat scope+tags, unlisted tags 2019-07-15 14:12:24 -05:00
multiple creatures 0a5eba734e add ability to export followers 2019-07-15 14:12:21 -05:00
multiple creatures 29643fd6c4 make blocking domains actually block the accounts 2019-07-15 14:12:16 -05:00
multiple creatures 992bd7c752 update `ruby-bbcode` to 2.1.0 2019-07-14 20:02:34 -05:00
multiple creatures a1d091b552 fix tag routing once and for all 2019-05-22 11:02:38 -05:00
multiple creatures 61ac01a6bb **security** - make sure local roars always get sanitized 2019-05-22 10:34:03 -05:00
multiple creatures 7b6f8e5419 remove push subscription schedulers from sidekiq 2019-05-22 01:24:11 -05:00
multiple creatures 2bdfbfe32c ignore the `tsv` column on the statuses table - our code will never touch this 2019-05-21 23:31:36 -05:00
multiple creatures ec288a11a0 who would win? controller of the tags or a `:` who skipped character class? 2019-05-21 22:24:59 -05:00
multiple creatures dd7164aac2 handle tags with the old `:` scope delimiter but translate those to `.` 2019-05-21 21:20:42 -05:00
multiple creatures 9abf1ce535 make sure only `self`/`self.` & `local`/`local.` tags are marked private & local + treat `:` in tags as `.` 2019-05-21 21:01:20 -05:00
multiple creatures 3c455a7b81 convince resource router that dots are good actually 2019-05-21 18:22:23 -05:00
multiple creatures 6c6d5319d9 `hashtag_scope` is a method in *this* class - how did we even manage to break that??? 2019-05-21 18:22:18 -05:00
multiple creatures d9b62e5d11 make canceling a reply not take your roar out with it 2019-05-21 16:44:12 -05:00
multiple creatures 58f78a7af2 fix `thread:reall` 2019-05-21 16:31:10 -05:00
multiple creatures d4ca04f24d upstream forgot to allow the import mode param 2019-05-21 03:17:31 -05:00
multiple creatures 641e5acc09 exceptions: gotta catch em all 2019-05-21 03:17:31 -05:00
multiple creatures d6f37c6ae0 handle importing posts from json dumps 2019-05-21 03:17:31 -05:00
multiple creatures cbdadfb5fa add `origin` to status table to mark posts that were previously imported 2019-05-21 03:16:51 -05:00
multiple creatures 62d667dbf5 add `imported` column to status table to mark posts imported from a json dump 2019-05-21 03:16:51 -05:00
multiple creatures bf33771c80 add `edited` column to status table to mark if a mod/admin changed something 2019-05-21 03:16:51 -05:00
multiple creatures 83c2c466fb use dots instead of colons for tag scopes 2019-05-21 03:16:51 -05:00
multiple creatures 55e0484121 add `skip_notify` option to service objects we might use for post imports 2019-05-21 03:16:51 -05:00
multiple creatures 811137ef69 if `created_at` is given to `PostStatusService` make sure it's utc 2019-05-21 03:16:51 -05:00
multiple creatures 8534702269 add missing `account_media_status_ids` method to accounts controller (oooops~) 2019-05-21 03:16:51 -05:00
multiple creatures 06b8b09fca add `keysmash` bangtag 2019-05-21 03:16:51 -05:00
multiple creatures 0f50698beb fix hoverplay emoji in bbcode 2019-05-21 03:16:51 -05:00
multiple creatures 01a5d51ef7 let account badges in web app wrap 2019-05-21 03:16:51 -05:00
multiple creatures 181d9cd24b styling for staff quick links 2019-05-21 03:16:51 -05:00
multiple creatures f0466aec02 space out badges 2019-05-21 03:16:51 -05:00
multiple creatures 8b47cdef24 add quick links for staff 2019-05-21 03:16:51 -05:00
multiple creatures 46216a4030 show mod & admin badges in web app 2019-05-21 03:16:51 -05:00
multiple creatures ee83fe92f1 expose account roles to the api 2019-05-21 03:16:51 -05:00
multiple creatures 1244c30349 add strings for new import options & change domain block -> domain policy 2019-05-21 03:16:51 -05:00
multiple creatures 45e4449347 clear status caches when someone is moderated force unlisted or force sensitive 2019-05-21 03:16:51 -05:00
multiple creatures 1fa6d6e16b limit search results to 33 until we split & paginate the search api 2019-05-21 03:16:51 -05:00
multiple creatures 8ff64df4dd render normalized status in reply indicator so that custom emojis are shown 2019-05-21 03:16:50 -05:00
multiple creatures 978ed2797f allow custom emojis in field keys in web app 2019-05-21 03:16:50 -05:00
multiple creatures c66d932c22 include bio field keys in emojifiable text 2019-05-21 03:16:50 -05:00
multiple creatures 5e2a8f3d3c remove clipping border from links 2019-05-21 03:16:50 -05:00
multiple creatures fd8e92438c formatted bios + merge monsterpit markdown styles directly into glitch-soc scss 2019-05-21 03:16:50 -05:00
multiple creatures 1a670573e5 apply monsterpit style overrides to public pages 2019-05-21 03:16:50 -05:00
multiple creatures 2705c6751e set correct name on media caption `div` 2019-05-21 03:16:50 -05:00
multiple creatures fd753d1201 add html to `format` bangtag + shortcut for bbdown` 2019-05-21 03:16:50 -05:00
multiple creatures 0a00a42c67 add bbdown & bbcode options to prefs menu 2019-05-21 03:16:50 -05:00
multiple creatures 23d2e5f97c fix bbdown newlines + format & emojify footers 2019-05-21 03:16:50 -05:00
multiple creatures e411b20711 correct media reveal wording 2019-05-21 03:16:50 -05:00
multiple creatures 7a0dc34cad reimplement monsterpit bbcode and markdown extensions on top of new glitch-soc formatting system + bbcode feature parity + new `i:am` footer + set content type from `format` bangtag 2019-05-21 03:16:50 -05:00
multiple creatures 09b7532805 split `i:am` signatures into their very own `footer` column 2019-05-21 03:16:50 -05:00
multiple creatures 5c9aed40f6 tell other ap software what content type they should expect source text to be 2019-05-21 03:16:23 -05:00
multiple creatures d70e5afb6e spell correctly 2019-05-21 03:16:23 -05:00
multiple creatures edeb344b90 re-add support for tags `sup`, `h6`, `hr` 2019-05-21 03:16:23 -05:00
multiple creatures 8394452bae fix schema 2019-05-21 03:16:23 -05:00
multiple creatures 7f460853c8 one lil zwnj to make sure compat sigs always get parsed correctly 2019-05-21 03:16:23 -05:00
multiple creatures f0c9477a4b compact `i:am` signatures when there are no newlines or block starters 2019-05-21 03:16:23 -05:00
multiple creatures 21c3730703 make large emojos 24px instead of 32px 2019-05-21 03:16:23 -05:00
multiple creatures dd021e8570 fix hover-to-play emoji clipping 2019-05-21 03:16:23 -05:00
multiple creatures 75d114216e remove the buggy do-not-@-me impl - we'll replace this with a bangtag later 2019-05-21 03:16:23 -05:00
multiple creatures 8a1ac19777 when autoplay is off play animated emoji on hover 2019-05-21 03:16:23 -05:00
multiple creatures 506d2e9cf0 add option to embiggen the emojos & embiggen small bbcode text on hover 2019-05-21 03:16:23 -05:00
multiple creatures e58efb8528 Strings: Replace the confusing "include(s)" with "address" (verb) and "mention" (noun). 2019-05-21 03:16:23 -05:00
multiple creatures 3b6f8ddacc Strings, Moderation: Replace "blocked domain" with "changed policy for" (re: commit 6eaf72). 2019-05-21 03:16:23 -05:00
multiple creatures c961429dc2 Accounts, UI: Expose kobold badges over the API. Render badges in the web app. 2019-05-21 03:16:23 -05:00
multiple creatures 89c5d8ec4e Custom filters: resolve bug where filter is erronously ignored when media filtering flags are unset. 2019-05-21 03:16:23 -05:00
multiple creatures a680595ecb `PostStatusService`: Add options to set a specific creation time and whether or not to distribute, useful for importing statuses. 2019-05-21 03:16:23 -05:00
multiple creatures 24a59d8f58 Correct a 6-in-the-morning breaking typos. 2019-05-21 03:16:23 -05:00
multiple creatures 47a251048c Expose user settings to `Account` model and update code referencing those settings. 2019-05-21 03:16:23 -05:00
multiple creatures 8d12242216 `ActivityPub::ProcessAccountService`: Correct `force_` method names. 2019-05-21 03:16:23 -05:00
multiple creatures 6834ddffc9 ActivityPub `Actor`: use correct property name for `suggestedMinAge`. 2019-05-21 03:16:23 -05:00
multiple creatures 3b06175e8f Moderation: add `force sensitive` and `force unlisted` actions. Accounts: add federatable `adult content` tag. Handle from remote accounts as well. 2019-05-21 03:16:23 -05:00
multiple creatures 5c59d1837f Make user settings for status filter accessible from `account` model. 2019-05-21 03:16:23 -05:00
multiple creatures 0782dc3905 Drop remaining OStatus and PuSH code, as well as related database items. 2019-05-21 03:16:23 -05:00
multiple creatures cb311a274c Remove unneeded migration. 2019-05-21 03:16:23 -05:00
multiple creatures a3faf5b169 Account model: drop `protocol` attribute when looking up AP inboxes; use `remote` scope instead. If no `domain` set, use domain of `inbox_url`. 2019-05-21 03:16:23 -05:00
multiple creatures c2e07ecd7f User model: give user settings getter methods better names; cache the settings queries. 2019-05-21 03:16:23 -05:00
multiple creatures edfabe44da Statuses: set `local_only` flag from the `PostStatusService`. Default to `en` locale for statuses. 2019-05-21 03:16:23 -05:00
multiple creatures 540728e063 `SettingsHelper`: return `HUMAN_LOCALES` keys directly 2019-05-21 03:16:23 -05:00
multiple creatures 82f691e9a9 Strings: correct broken username interpolations while reflecting upon the dangers of automated nomenclature changes using `sed`. 2019-05-21 03:16:23 -05:00
multiple creatures 976ec97ffe Bangtags: do not assume `status.in_reply_to_id` exists just because `status.reply?` is `true`. 2019-05-21 03:16:23 -05:00
multiple creatures e85b8af051 Second round of Rspec fixes. 2019-05-21 03:16:23 -05:00
multiple creatures 66886d4367 RSpec: Test for correct media description limit. 2019-05-21 03:16:23 -05:00
multiple creatures dca70079b1 Remove automatic language detection. 2019-05-21 03:16:23 -05:00
multiple creatures 6c374b5153 Drop OStatus support. Fix some of the Rspec tests. 2019-05-21 03:16:23 -05:00
multiple creatures 6e8ec7f0a5 Allow passing an array of tags to `PostStatusservice` 2019-05-21 03:16:22 -05:00
multiple creatures 9f9ee606f3 Revise status filter logic. 2019-05-21 03:16:22 -05:00
multiple creatures e0c6d56f5f Adjust Glitch UI defaults 2019-05-21 03:16:22 -05:00
multiple creatures 59fd9c25dc Replies should not exceed the visibility being replied to. 2019-05-21 03:16:22 -05:00
multiple creatures feea4f6dc0 Add line-break-sized padding after lists and blockquotes. 2019-05-21 03:16:22 -05:00
multiple creatures 46522d8c1b Do not process mentions or bangtags in drafts. Add `draft?` method to `Status` model. 2019-05-21 03:16:22 -05:00
multiple creatures 3e8690f2c0 Correct breaking type-o: `scope` -> `starting_scope` 2019-05-21 03:16:22 -05:00
multiple creatures 163d42c04a Buttons expecting users to perform an action should be verbs. 2019-05-21 03:16:22 -05:00
multiple creatures 1ed7aca171 Add `parent:tag` bangtag 2019-05-21 03:16:22 -05:00
multiple creatures a1be3a11a9 Don't try to add tags that already exist in the status. 2019-05-21 03:16:22 -05:00
multiple creatures 2f23d34e36 Add header to draft roars. 2019-05-21 03:16:22 -05:00
multiple creatures db6ae92c09 Limit tag scope nesting to six components. Rewrite multiple consecutive delimiters to one (`::`, `:::`, ... => `:`). 2019-05-21 03:16:22 -05:00
multiple creatures 726a99a6e4 Add `draft` and `visibility` bangtags. Wrap `ProcessHashtagsService` to update status tags. 2019-05-21 03:16:22 -05:00
multiple creatures 7ca4a2089c Update schema. 2019-05-21 03:16:22 -05:00
multiple creatures ecf21d3fc6 Strings: add UI strings for interaction list options; clearer wording of `hidden`, `setting_hide_network`, and `invite_request`. 2019-05-21 03:16:22 -05:00
multiple creatures c983c4e952 Privacy: add options to make interaction lists private and to not be included in public interaction lists. 2019-05-21 03:16:22 -05:00
multiple creatures a47b1daaeb Implement scoped tags; use `local:` and `self:` scopes for community and personal tags, respectively. 2019-05-21 03:16:22 -05:00
multiple creatures 992218f05f Anxiety reduction: add option to hide mascot. 2019-05-21 03:16:22 -05:00
multiple creatures 545330dc65 federate raw versions of roars using `source` prop 2019-05-21 03:16:22 -05:00
multiple creatures a7015f9202 add `parent:emoji` and `thread:emoji` bangtags 2019-05-21 03:16:22 -05:00
multiple creatures 515688c547 stop wasting server resources converting well-supported open video formats just because a single mobile platform's proprieter wants to be a jerk 2019-05-21 03:16:22 -05:00
multiple creatures 79cc6792a1 Adjust search limits and ordering. 2019-05-21 03:16:22 -05:00
multiple creatures cfaed183aa filter prop is sometimes `undefined` i guess??? 2019-05-21 03:16:22 -05:00
multiple creatures db67333d62 Add option to filter packmate thread branches where you don't follow all the participants. 2019-05-21 03:16:22 -05:00
multiple creatures 933d7afa87 Make the UI properly handle filters using `exclude_media` and `media_only` flags. 2019-05-21 03:16:22 -05:00
multiple creatures 8dbefa3966 Assign a `key` prop to get rid of warning. 2019-05-21 03:16:22 -05:00
multiple creatures 1a12429051 Correct name broken by automated nomenclature replacement. 2019-05-21 03:16:22 -05:00
multiple creatures 1e2977256c Allow own roars to be included in lists. 2019-05-21 03:16:22 -05:00
multiple creatures 89e54748d7 Don't show Roars & Growls tag in web app if someone has disabled showing public replies. 2019-05-21 03:16:22 -05:00
multiple creatures e87151f458 Exclude repeats on the roars tab. 2019-05-21 03:16:22 -05:00
multiple creatures 1ab60fea48 Split boosts/repeats off to their own tab and add to the API. 2019-05-21 03:16:22 -05:00
multiple creatures ed9c8f67c4 Cache in memory for 30 minutes. 2019-05-21 03:16:22 -05:00
multiple creatures 13262ea614 Remove cached status when sharekey changed. 2019-05-21 03:16:22 -05:00
multiple creatures 26d90a36ff Custom filters: add ability to create filters that exclude or are exclusive to roars with attachments. 2019-05-21 03:16:22 -05:00
multiple creatures 2423830e3c Don't crash if payload is undefined. 2019-05-21 03:16:22 -05:00
multiple creatures d339d2bbb4 Make sure signatures start a new paragraph and that existing signatures are detected correctly. 2019-05-21 03:16:22 -05:00
multiple creatures 4644a6245f Raise pack timeline windows. 2019-05-21 03:16:22 -05:00
multiple creatures 4d12e45d3b Account manager: consider dormant to be 3 months instead of one. 2019-05-21 03:16:22 -05:00
multiple creatures f573712f82 Bangtags: ignore case of commands 2019-05-21 03:16:22 -05:00
multiple creatures d8f182d235 Make sure parent account excists. 2019-05-21 03:16:22 -05:00
multiple creatures 7ce2e174cf Group like media captions together and remove space between descriptors. 2019-05-21 03:16:22 -05:00
multiple creatures 9d4f42fb89 Add option to hide public replies from profile 2019-05-21 03:16:22 -05:00
multiple creatures fb449f273a `i:am`: Right-align signatures 2019-05-21 03:16:22 -05:00
multiple creatures 2a5784c61f 2 newlines is plently but 3 is way too many 2019-05-21 03:16:22 -05:00
multiple creatures c1d2febf03 these aint methods silly 2019-05-21 03:16:22 -05:00
multiple creatures 05ed9b6cea `i:am`: replace horizontal rule with ZWNJ. 2019-05-21 03:16:22 -05:00
multiple creatures 534e19cbe3 Stylistic: use em dash for signature prefix. 2019-05-21 03:16:22 -05:00
multiple creatures af7e3a88d4 Anxiety reduction: make block/mute reply filters more granular and add options to control what to filter. 2019-05-21 03:16:22 -05:00
multiple creatures 15b35d99ce `i:am`: Do not add a signature if the author inserted their own. 2019-05-21 03:16:22 -05:00
multiple creatures 66c640fd38 Allow sharekeys to be set via the API. 2019-05-21 03:16:22 -05:00
multiple creatures bb9aa16284 Make sure `rekey` parameter exists before complaining. 2019-05-21 03:16:22 -05:00
multiple creatures 87f4b4d230 Implement share keys and related bangtags, add `sharekey`, `network`, and `curated` to the API, remove app info from the UI, and move timestamps to the right. 2019-05-21 03:16:22 -05:00
multiple creatures 19b78604e9 Status model: `LOCAL_DOMAINS` constant has been moved into the `Account` model. 2019-05-21 03:16:22 -05:00
multiple creatures a230a6038b Make sure that local-only tokens still get detected when signature is enabled. 2019-05-21 03:16:22 -05:00
multiple creatures 4088e0a648 Add `i:am` bangtag to allow plural systems sharing an account to identify who is roaring. 2019-05-21 03:16:22 -05:00
multiple creatures f7c5171a83 DB: Replace `NULL` boolean values with `FALSE` in Monsterpit feature columns; add `vars` column for persistent bangtag variable storage. 2019-05-21 03:16:22 -05:00
multiple creatures b8b525c54a Correct nomenclature changes that break strings. 2019-05-21 03:16:22 -05:00
multiple creatures 9753fd203e Raise various string limits. 2019-05-21 03:16:22 -05:00
multiple creatures 1fe28ca9d6 Extend limits for poll time range to between a minute and 6 months. 2019-05-21 03:16:22 -05:00
multiple creatures fb47b6e120 Make sure topmost image fills the gap in an odd-numbered media set. 2019-05-21 03:16:22 -05:00
multiple creatures 71302f6dec Handle up to 6 attachments per roar. 2019-05-21 03:16:22 -05:00
multiple creatures ea40ae8de7 Change stray reblog SVG to repeat glyph. 2019-05-21 03:16:22 -05:00
multiple creatures acdfce2bba Raise maximum attachment size to 66 MiB and maximum standard GIF size to 333 KiB. 2019-05-21 03:16:22 -05:00
multiple creatures 1ca30982fa Add support for general-purpose file sharing. 2019-05-21 03:16:22 -05:00
multiple creatures b53ddb8126 Bump Ruby version to 2.6.3. 2019-05-21 03:16:22 -05:00
multiple creatures adea831c00 Add Pending Accounts tab to the Moderation section. 2019-05-21 03:16:22 -05:00
multiple creatures 036f422877 Make sure only distributable statuses are marked curated; move bangtags processing into own helper lib. 2019-05-21 03:16:22 -05:00
multiple creatures 500b485b77 Raise invite request text limit. Carry a kobold on tail. 2019-05-21 03:16:22 -05:00
multiple creatures cea2baf2e0 Limit scope of local/world TLs. Fixes the bug causing some requests to fetch the local and world timelines to time out. 2019-05-21 03:16:22 -05:00
multiple creatures ec5c6e7fcb This `relay` should be a string, not a `Symbol`. 2019-05-21 03:16:22 -05:00
multiple creatures 3bfa72cbce Dedicated `network` DB column for marking whether a roar is a part of the local network; rewrite posts from `FORCE_*` domains at create time instead of dynamically. 2019-05-21 03:16:22 -05:00
multiple creatures cdacbb3c4c Add option to remove filtered roar placeholder gap. 2019-05-21 03:16:22 -05:00
multiple creatures 50fae175fd Tenatacle skin: remove ununsed classes 2019-05-21 03:16:22 -05:00
multiple creatures 021fedeb2a Replace `fa-retweet` SVG with `fa-repeat` Unicode glyph. 2019-05-21 03:16:22 -05:00
multiple creatures 841776edfb Add minimum timeframe for a self-destructing roar in preparating for implementing. 2019-05-21 03:16:22 -05:00
multiple creatures 3f282fe433 Search UI: center hashtag results 2019-05-21 03:16:22 -05:00
multiple creatures 340f1e9149 Compact the hashtag section of the search UI. Get rid of trending sections; they do not work properly on instances with less than 10,000 active users. 2019-05-21 03:16:22 -05:00
multiple creatures dfbc2fc518 Add options to increase size and spacing of action buttons and width of compose drawer. 2019-05-21 03:16:22 -05:00
multiple creatures 1930b2332d Limit width of single-column to 600px, keep composer docked to left. 2019-05-21 03:16:22 -05:00
multiple creatures beee1934b2 Place roar and hashtag sections at the top in search UI. 2019-05-21 03:16:22 -05:00
multiple creatures 08a32175fa Add scope restrictions on curated world timeline considerations. 2019-05-21 03:16:22 -05:00
multiple creatures cec0a7ff3c Make sure that replies that are not sections of a root thread get filtered out of the world timeline. 2019-05-21 03:16:22 -05:00
multiple creatures 89ad628e88 Add `share_key` column to status table in preparation for letting folks generate/revoke links to view private posts. 2019-05-21 03:16:22 -05:00
multiple creatures abb8848eb7 Add `share_key` column to status table in preparation for letting folks generate/revoke links to view private posts. 2019-05-21 03:16:21 -05:00
multiple creatures 1823e78aa7 Do not set default in `add_column` migration. 2019-05-21 03:16:21 -05:00
multiple creatures 2db51e2f4c Refactored community-curated world timeline code; **privacy**: remove support for packmate-visible hashtags until we resolve federation caveats. 2019-05-21 03:16:21 -05:00
multiple creatures c86c4b95be Raise search result limit to 33 in frontend. 2019-05-21 03:16:21 -05:00
multiple creatures f344170fd0 Raise max search result limit to 66 in API. 2019-05-21 03:16:21 -05:00
multiple creatures 6c7f1691ee Only consider favorites for community-curated world timeline until we can make the query for considering boosts not be so abysmally slow. 2019-05-21 03:16:21 -05:00
multiple creatures 16147d73a2 `Status.search_for`: Don't need `Status.unscoped`; the `default_scope` sorts the way we intend. 2019-05-21 03:16:21 -05:00
multiple creatures dd5e02ad5d Use PostgreSQL FTS for searches. 2019-05-21 03:16:21 -05:00
multiple creatures 4c170d2a98 Add options to increase size and spacing of context menus and to gently the kobolds. Also allow showing multiple user roles. 2019-05-21 03:16:21 -05:00
multiple creatures d033327136 Apply GIFv styling to regular GIF overlay as well 2019-05-21 03:16:21 -05:00
multiple creatures 2cc2089534 Add support for standard GIFs (under 200 KB) 2019-05-21 03:16:21 -05:00
multiple creatures cd042a4ee3 Handle more audio formats, only converting formats not supported by HTML5 audio 2019-05-21 03:16:21 -05:00
multiple creatures 7370ff5677 let the fields overflow~ 2019-05-21 03:16:21 -05:00
multiple creatures 4550d9188d adjust upstream invite wording 2019-05-21 03:16:21 -05:00
multiple creatures 0e5935e475 various bangtag improvements squashed into one commit 2019-05-21 03:16:21 -05:00
multiple creatures 8d19acc618 allow users to add emoji 2019-05-21 03:16:21 -05:00
multiple creatures caf265bbeb add option to toggle captions + code: move monsterpit additons on top 2019-05-21 03:16:21 -05:00
multiple creatures 2ee72d3aaf s/Toot/Roar/ 2019-05-21 03:16:21 -05:00
multiple creatures bc77147a95 make hide public stats option more clear 2019-05-21 03:16:21 -05:00
multiple creatures d7dd432727 also put hide stats on public pages under mouseover 2019-05-21 03:16:21 -05:00
multiple creatures 618627eb12 make hide stats option hide all public page stats 2019-05-21 03:16:21 -05:00
multiple creatures 8938e343de Don't make stats prominent - hide them in mouseovers 2019-05-21 03:16:21 -05:00
multiple creatures c85e467c0c group related prefs together 2019-05-21 03:16:21 -05:00
multiple creatures d0631f446c bangtags: support namespacing, args, text tf + add replace commands 2019-05-21 03:16:21 -05:00
multiple creatures e42f09c53d curated world: ignore non-self replies 2019-05-21 03:16:21 -05:00
multiple creatures 7580036307 curated world: also consider public favs 2019-05-21 03:16:21 -05:00
multiple creatures b507a598c5 add bangtags to reply-all thead, insert zws; handle fencing in braces 2019-05-21 03:16:21 -05:00
multiple creatures 01acaa792a bangtag to mention admin/mods/staff 2019-05-21 03:16:21 -05:00
multiple creatures d00907014b bangtags for cloudroot (monsterpit.cloud/~/you) and blogroot (monsterpit.blog/~/you) links 2019-05-21 03:16:21 -05:00
multiple creatures dd70b4e463 initial bangtags implementation, permalinks 2019-05-21 03:16:21 -05:00
multiple creatures 8bf596861b move extended description presenter to own method 2019-05-21 03:16:21 -05:00
multiple creatures 6614d42c6e hidden accounts + stats hiding 2019-05-21 03:16:21 -05:00
multiple creatures 90d2280dfe actually add raw world to settings controller <.<" >.> 2019-05-21 03:16:21 -05:00
multiple creatures 5284cdc24d correct typo 2019-05-21 03:16:21 -05:00
multiple creatures ba51d3f135 add option to show raw world timeline 2019-05-21 03:16:21 -05:00
multiple creatures 3e8d7fd5f8 initial port of daggertooth!monsterpit themes 2019-05-21 03:16:21 -05:00
multiple creatures 475ef8bbf1 don't lighten footer drawer 2019-05-21 03:16:21 -05:00
multiple creatures 02d5a52673 restore default drawer 2019-05-21 03:16:21 -05:00
multiple creatures 85a9dee905 restore custom registration message 2019-05-21 03:16:21 -05:00
multiple creatures 57113accf6 center free-standing widget text 2019-05-21 03:16:21 -05:00
multiple creatures 3a69196c47 allow aac audio uploads 2019-05-21 03:16:21 -05:00
multiple creatures f920593cd4 use ruby 2.6.2 2019-05-21 03:16:21 -05:00
multiple creatures 6c00e2abcf update status.rb 2019-05-21 03:16:21 -05:00
multiple creatures dc5993191d arrange widgets in a sensible order 2019-05-21 03:16:21 -05:00
multiple creatures e84f1bc4ef don't sticky local only setting 2019-05-21 03:16:21 -05:00
multiple creatures 758deeb818 update csp for img proxy 2019-05-21 03:16:21 -05:00
multiple creatures 312bc14d06 make sure local only replies stay local 2019-05-21 03:16:21 -05:00
multiple creatures 0554e7c3bd single-column glitch theme 2019-05-21 03:16:21 -05:00
multiple creatures cbbed1863a sort tab bar order 2019-05-21 03:16:21 -05:00
multiple creatures 393f8aa4e1 unbreak some translation entries 2019-05-21 03:16:21 -05:00
multiple creatures bd55c63d93 adjust poll text top padding & make text even 2019-05-21 03:16:21 -05:00
multiple creatures 366676a69f make sure that poll text is actually smaller 2019-05-21 03:16:21 -05:00
multiple creatures 2f136e9889 allow text wrapping in polls + smaller font 2019-05-21 03:16:21 -05:00
multiple creatures b57383e025 increase polls to 6 opts x 111 chars 2019-05-21 03:16:21 -05:00
multiple creatures 0bdb555f57 s/toot/roar/ 2019-05-21 03:16:21 -05:00
multiple creatures b5e6e77ca4 glitch: fix local only matching 2019-05-21 03:16:21 -05:00
multiple creatures f21d4c3209 allow eye emoji to be a local-only flag 2019-05-21 03:16:21 -05:00
multiple creatures 3e55dcf944 don't strip emoji metadata 2019-05-21 03:16:21 -05:00
multiple creatures ff02142601 show captions for media descriptions 2019-05-21 03:16:21 -05:00
multiple creatures 467170f4a0 more local-only options 2019-05-21 03:16:21 -05:00
multiple creatures b5cb68581b actually add defaultLocal state (oops) 2019-05-21 03:16:21 -05:00
multiple creatures 1affcf73fb add option to default to local only 2019-05-21 03:16:21 -05:00
multiple creatures 9d559d790b default visibility public on new accounts 2019-05-21 03:16:21 -05:00
multiple creatures ace01a82da let local only option be sticky 2019-05-21 03:16:21 -05:00
multiple creatures 0697f20f2c Automatic nomenclature update 2019-05-21 03:16:21 -05:00
multiple creatures 178a2dc9eb hide follower-only replies on account tls 2019-05-21 03:16:21 -05:00
multiple creatures f1ed7bb675 hide filtered message 2019-05-21 03:16:21 -05:00
multiple creatures 28c9b9ce6a improve filtering 2019-05-21 03:16:21 -05:00
multiple creatures 10b20607ac allow hypens in hashtags 2019-05-21 03:16:21 -05:00
multiple creatures 1d5da39902 allow overriding visibility/sensitivity by domain 2019-05-21 03:16:21 -05:00
multiple creatures d6738df083 handle no-replies flag 2019-05-21 03:16:21 -05:00
multiple creatures 1636a4e8ae unlisted & private hashtags 2019-05-21 03:16:21 -05:00
multiple creatures a7aa2544e4 community world tl + networked home tl 2019-05-21 03:16:21 -05:00
multiple creatures 9a94d5e2c5 transparency: let logged in users view mod actions 2019-05-21 03:16:21 -05:00
multiple creatures fa7e6b5a63 Add pseudomentions to various hosts 2019-05-21 03:16:21 -05:00
multiple creatures 5c6a14ca68 Prune replies to muted/blocked accounts 2019-05-21 03:16:21 -05:00
multiple creatures cf557f1849 Plain AAC audio uploads live again. -DT 2019-05-21 03:16:21 -05:00
Thibaut Girka f119ef489c [Glitch] Adds follow action timestamp to notification
Port 330e320b40 to glitch-soc
2019-05-21 03:16:21 -05:00
multiple creatures 6a0c65d461 allow searching bookmarks 2019-05-21 03:16:21 -05:00
multiple creatures 706c177c8e also make packmate roars pinnable in web app 2019-05-21 03:16:21 -05:00
multiple creatures 3c17de7724 allow pinning packmate roars 2019-05-21 03:16:20 -05:00
multiple creatures 17a7aeb807 additional secure headers 2019-05-21 03:16:20 -05:00
multiple creatures 918f7b7478 disallow indexing 2019-05-21 03:16:20 -05:00
Daggertooth 3f79556155 Change default colmns in Glitch flavor, too. 2019-05-21 03:16:20 -05:00
Daggertooth b90ad78073 Change default columns for new users 2019-05-21 03:16:20 -05:00
Daggertooth 28724df663 Let moderators manage blocklist 2019-05-21 03:16:20 -05:00
Daggertooth 49353be9f1 Let moderators manage Emoji 2019-05-21 03:16:20 -05:00
Daggertooth 5db1301d7a Raise description text max length 2019-05-21 03:16:20 -05:00
Daggertooth b8ef3027da Raise description text max length 2019-05-21 03:16:20 -05:00
Daggertooth 41642c6668 Allow 4k image uploads 2019-05-21 03:16:20 -05:00
Daggertooth 70649ca277 Store more statuses in home timeline 2019-05-21 03:16:20 -05:00
Daggertooth 71066e4ad6 Return more accounts in searches 2019-05-21 03:16:20 -05:00
Daggertooth a607195dc0 Email mention notifications by default 2019-05-21 03:16:20 -05:00
Daggertooth d0d22f3c68 Opt users out of search engine indexing by default 2019-05-21 03:16:20 -05:00
Daggertooth cf3aee91ca Confirm boosts by default 2019-05-21 03:16:20 -05:00
Daggertooth ae1576691f Confirm unfollows by default 2019-05-21 03:16:20 -05:00
Daggertooth 4df5e5c01c Rename default site title 2019-05-21 03:16:20 -05:00
483 changed files with 9792 additions and 7948 deletions

View File

@ -160,6 +160,27 @@ STREAMING_CLUSTER_NUM=1
# Maximum number of pinned posts
# MAX_PINNED_TOOTS=5
# Maximim number of profile fields allowed
# MAX_PROFILE_FIELDS=4
# Maximum allowed display name characters
# MAX_DISPLAY_NAME_CHARS=30
# Maximum image and video upload sizes
# Units are in megabytes
# MAX_SIZE_LIMIT=66
# Maximum gif size limit
# Units are in kilobytes
# MAX_GIF_SIZE=333
# Maximum length of audio uploads in seconds
# MAX_AUDIO_LENGTH=60
# Maximum number of search results
# Only really matters if elasticsearch is enabled
# MAX_SEARCH_RESULTS=100
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost

2
.gitignore vendored
View File

@ -58,3 +58,5 @@ yarn-debug.log
# Ignore Docker option files
docker-compose.override.yml
# ignore misc directory
/misc

View File

@ -1 +1 @@
2.6.1
2.6.3

View File

@ -30,7 +30,6 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.4'
gem 'devise', '~> 4.6'
gem 'devise-two-factor', '~> 3.0'
@ -62,7 +61,6 @@ gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.7'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.10'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.0'
@ -151,3 +149,5 @@ group :production do
end
gem 'concurrent-ruby', require: false
gem "ruby-bbcode", "~> 2.0"

View File

@ -146,8 +146,6 @@ GEM
elasticsearch (>= 2.0.0)
elasticsearch-dsl
chunky_png (1.3.10)
cld3 (3.2.4)
ffi (>= 1.1.0, < 1.11.0)
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
@ -382,10 +380,6 @@ GEM
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
orm_adapter (0.5.0)
ostatus2 (2.0.3)
addressable (~> 2.5)
http (~> 3.0)
nokogiri (~> 1.8)
ox (2.10.0)
paperclip (6.0.0)
activemodel (>= 4.2.0)
@ -539,6 +533,8 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
ruby-bbcode (2.1.0)
activesupport (>= 4.2.2)
ruby-progressbar (1.10.0)
ruby-saml (1.9.0)
nokogiri (>= 1.5.10)
@ -681,7 +677,6 @@ DEPENDENCIES
capybara (~> 3.20)
charlock_holmes (~> 0.7.6)
chewy (~> 5.0)
cld3 (~> 3.2.4)
climate_control (~> 0.2)
concurrent-ruby
derailed_benchmarks
@ -728,7 +723,6 @@ DEPENDENCIES
omniauth (~> 1.9)
omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10)
ostatus2 (~> 2.0)
ox (~> 2.10)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
@ -758,6 +752,7 @@ DEPENDENCIES
rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0)
rubocop (~> 0.69)
ruby-bbcode (~> 2.0)
sanitize (~> 5.0)
scss_lint (~> 0.58)
sidekiq (~> 5.2)

View File

@ -1,12 +1,69 @@
# Mastodon Glitch Edition #
# Monsterfork
> Now with automated deploys!
> *[Monsterpit](https://monsterpit.net/about/more) is a community of creatures and critters* /
> *For those who love monsters to be monsters they love.* /
> *Whether fur, scale, or skin; whether plural or kin* /
> *If you dont feel quite human, come!* /
> *Youll fit right on in.*
[![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci]
Monsterfork is a... well... fork of [Glitch-Soc](https://glitch-soc.github.io) used on [Monsterpit](https://monsterpit.net/about). It focuses on adding a *monstrous* number of community features with wild abandon along with improved accessibility, better moderation tools, and more user privacy options.
[circleci]: https://circleci.com/gh/glitch-soc/mastodon
## Non-exhaustive feature list
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
### Identity
- [Signatures](https://monsterpit.blog/monsterpit-bangtags/i-am)
- Account switching
- You can view 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/).
### Advanced
- [Bangtag macros](https://monsterpit.blog/monsterpit-bangtags)
### Privacy
- [Sharekeys](https://monsterpit.blog/monsterpit-bangtags/sharekey-new)
- Self-destructing posts
- Optional public profile pages and ActivityPub outbox
- Option to limit the length of time posts are avaiable
### Accessibility
- Media descriptions shown as captions in UI by default
- High-contrast visibility icons by default
- UI element size and spacing options
### Boundries
- Respect "don't `@` me"
- All threads can be muted
### Anxiety reduction
- No metrics in the UI
- Additional post and thread filtering options
- Granular visibility options
- [Community-curated world timeline](https://monsterpit.blog/monsterpit-creature-comforts/world-timeline)
### Publishing
- Delayed posts
- Queued boosts
- Formatting (BBdown, BBcode, Markdown, HTML, console, plain)
- Arbitary attachments
### Tagging
- Scoped tags (`#monsters.kobolds`, `#local.minotaur.den` `#self.drafts`)
- Unlisted tags (`#.hidden`)
- Retroactive tagging (`#!parent:tag:art`)
- Out-of-body tags
- Glitch-Soc bookmarks as a tag (`#self.bookmarks`)
### Imports
- Users can add their own custom emoji
- Emoji can be imported from other posts (`#!parent:emoji`) or threads (`#!thread:emoji`)
- Post importing from other ActivityPub software (currently text only)
### Moderation
- Additional policies (force unlisted, force sensitive, reject unknown)
- Moderator bangtags (`#!admin:silence`, `#!admin:suspend`, `#!admin:reset`, ...)
- New admin transparancy log system, posted under a tag
- Domain policy comments and list (`https://instance.site/policies`)
### Safety
- Graylist-based federation by default
- Domain suspensions include subdomains
- Can block malicious servers by ActivityPub object propreties
- Tools to block resource requests (see `/dist`)

View File

@ -47,6 +47,11 @@ class StatusesIndex < Chewy::Index
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :bookmarks do |collection|
data = ::Bookmark.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long'

View File

@ -11,6 +11,12 @@ class AccountsController < ApplicationController
respond_to do |format|
format.html do
use_pack 'public'
unless current_account && current_account.id == @account.id
not_found if @account.hidden
if @account&.user && @account.user.hides_public_profile?
not_found unless current_account && current_account.following?(@account)
end
end
mark_cacheable! unless user_signed_in?
@body_classes = 'with-modals'
@ -22,7 +28,8 @@ class AccountsController < ApplicationController
return
end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@pinned_statuses = cache_collection(pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status)
@ -32,20 +39,6 @@ class AccountsController < ApplicationController
end
end
format.atom do
mark_cacheable!
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? || entry.status.local_only? }))
end
format.rss do
mark_cacheable!
@statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses)
end
format.json do
mark_cacheable!
@ -58,39 +51,50 @@ class AccountsController < ApplicationController
private
def pinned_statuses
if user_signed_in? && current_account.following?(@account)
@account.pinned_statuses
else
@account.pinned_statuses.where.not(visibility: :private)
end
end
def show_pinned_statuses?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
[reblogs_requested?, replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested?
if reblogs_requested?
scope = default_statuses.reblogs
elsif replies_requested?
scope = @account.replies ? default_statuses : default_statuses.without_replies
elsif media_requested?
scope = default_statuses.where(id: account_media_status_ids)
elsif tag_requested?
scope = hashtag_scope
else
scope = default_statuses.without_replies.without_reblogs
end
return scope if current_user
return Status.none unless @account&.user
scope.where(created_at: @account.user.max_public_history.to_i.days.ago..Time.current)
end
def default_statuses
@account.statuses.not_local_only.where(visibility: [:public, :unlisted])
end
def only_media_scope
Status.where(id: account_media_status_ids)
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end
def no_replies_scope
Status.without_replies
end
def hashtag_scope
tag = Tag.find_normalized(params[:tag])
if tag
Status.tagged_with(tag.id)
return Status.none if !user_signed_in? && (tag.local || tag.private) || tag.private && current_account.id != @account.id
scope = tag.private ? current_account.statuses : tag.local ? Status.local : Status
scope.tagged_with(tag.id)
else
Status.none
end
@ -115,6 +119,8 @@ class AccountsController < ApplicationController
short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
elsif reblogs_requested?
short_account_reblogs_url(@account, max_id: max_id, min_id: min_id)
else
short_account_url(@account, max_id: max_id, min_id: min_id)
end
@ -128,6 +134,10 @@ class AccountsController < ApplicationController
request.path.ends_with?('/with_replies')
end
def reblogs_requested?
request.path.ends_with?('/reblogs')
end
def tag_requested?
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end

View File

@ -35,7 +35,7 @@ class ActivityPub::CollectionsController < Api::BaseController
def set_size
case params[:id]
when 'featured'
@account.pinned_statuses.count
@account.pinned_statuses.where.not(visibility: :private).count
else
raise ActiveRecord::RecordNotFound
end
@ -45,7 +45,7 @@ class ActivityPub::CollectionsController < Api::BaseController
case params[:id]
when 'featured'
@account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
scope.merge!(@account.pinned_statuses)
scope.merge!(@account.pinned_statuses.where.not(visibility: :private))
end
else
raise ActiveRecord::RecordNotFound

View File

@ -10,7 +10,6 @@ class ActivityPub::InboxesController < Api::BaseController
if unknown_deleted_account?
head 202
elsif signed_request_account
upgrade_account
process_payload
head 202
else
@ -38,16 +37,6 @@ class ActivityPub::InboxesController < Api::BaseController
@body
end
def upgrade_account
if signed_request_account.ostatus?
signed_request_account.update(last_webfingered_at: nil)
ResolveAccountWorker.perform_async(signed_request_account.acct)
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
end
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
end

View File

@ -55,8 +55,15 @@ class ActivityPub::OutboxesController < Api::BaseController
def set_statuses
return unless page_requested?
account_owner = current_account && current_account.id == @account.id
outbox_hidden = @account&.user && @account.user.hides_public_outbox?
local_follower = current_account && current_account.following?(@account)
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
if account_owner || !@account.hidden? || (outbox_hidden && local_follower)
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
else
@statuses = Status.none
end
@statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
@statuses = cache_collection(@statuses, Status)
end

View File

@ -2,8 +2,8 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :mark_known, :mark_unknown, :allow_public, :allow_nonsensitive, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
def index
@ -19,18 +19,6 @@ module Admin
@warnings = @account.targeted_account_warnings.latest.custom
end
def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def memorialize
authorize @account, :memorialize?
@account.memorialize!
@ -57,6 +45,48 @@ module Admin
redirect_to admin_accounts_path(pending: '1')
end
def mark_unknown
authorize @account, :mark_unknown?
@account.mark_unknown!
log_action :mark_unknown, @account
redirect_to admin_account_path(@account.id)
end
def mark_known
authorize @account, :mark_known?
@account.mark_known!
log_action :mark_known, @account
redirect_to admin_account_path(@account.id)
end
def force_sensitive
authorize @account, :force_sensitive?
@account.force_sensitive!
log_action :force_sensitive, @account
redirect_to admin_account_path(@account.id)
end
def allow_nonsensitive
authorize @account, :allow_nonsensitive?
@account.allow_nonsensitive!
log_action :allow_nonsensitive, @account
redirect_to admin_account_path(@account.id)
end
def force_unlisted
authorize @account, :force_unlisted?
@account.force_unlisted!
log_action :force_unlisted, @account
redirect_to admin_account_path(@account.id)
end
def allow_public
authorize @account, :allow_public?
@account.allow_public!
log_action :allow_public, @account
redirect_to admin_account_path(@account.id)
end
def unsilence
authorize @account, :unsilence?
@account.unsilence!

View File

@ -7,7 +7,7 @@ module Admin
layout 'admin'
before_action :require_staff!
#before_action :require_staff!
before_action :set_pack
before_action :set_body_classes

View File

@ -2,36 +2,32 @@
module Admin
class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy]
before_action :set_domain_block, only: [:show, :destroy, :update]
def new
authorize :domain_block, :create?
@domain_block = DomainBlock.new(domain: params[:_domain])
@domain_block = DomainBlock.new(domain: params[:_domain].present? ? params[:_domain].strip : nil)
end
def create
authorize :domain_block, :create?
resource_params[:domain].strip! if resource_params[:domain].present?
resource_params[:reason].strip! if resource_params[:reason].present?
@domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain].strip) : nil
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save
flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
@domain_block.errors[:domain].clear
render :new
if existing_domain_block.present?
@domain_block = existing_domain_block
@domain_block.update(resource_params.except(:undo))
end
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_instance_path(id: @domain_block.domain, limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
if existing_domain_block.present?
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
end
render :new
end
end
@ -41,9 +37,26 @@ module Admin
def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block)
DomainUnblockWorker.perform_async(@domain_block.id)
log_action :destroy, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
flash[:notice] = I18n.t('admin.domain_blocks.destroyed_msg')
redirect_to controller: 'admin/instances', action: 'index', limited: '1'
end
def update
return destroy unless resource_params[:undo].to_i.zero?
resource_params[:reason].strip! if resource_params[:reason].present?
authorize @domain_block, :update?
@domain_block.update(resource_params.except(:domain, :undo))
changed = @domain_block.changed
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id) if (changed & %w(severity force_sensitive reject_media reject_unknown)).any?
log_action :update, @domain_block
flash[:notice] = I18n.t('admin.domain_blocks.updated_msg')
else
flash[:alert] = I18n.t('admin.domain_blocks.update_failed_msg')
end
redirect_to admin_instance_path(id: @domain_block.domain, limited: '1')
end
private
@ -53,7 +66,7 @@ module Admin
end
def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports, :reject_unknown, :reason, :undo)
end
end
end

View File

@ -34,7 +34,7 @@ module Admin
helper_method :paginated_instances
def ordered_instances
paginated_instances.map { |resource| Instance.new(resource) }
paginated_instances.map { |resource| Instance.new(resource) }.sort_by(&:updated_at).reverse!
end
def filter_params

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
module Admin
class SubscriptionsController < BaseController
def index
authorize :subscription, :index?
@subscriptions = ordered_subscriptions.page(requested_page)
end
private
def ordered_subscriptions
Subscription.order(id: :desc).includes(:account)
end
def requested_page
params[:page].to_i
end
end
end

View File

@ -1,73 +0,0 @@
# frozen_string_literal: true
class Api::PushController < Api::BaseController
include SignatureVerification
def update
response, status = process_push_request
render plain: response, status: status
end
private
def process_push_request
case hub_mode
when 'subscribe'
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
when 'unsubscribe'
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
else
["Unknown mode: #{hub_mode}", 422]
end
end
def hub_mode
params['hub.mode']
end
def hub_topic
params['hub.topic']
end
def hub_callback
params['hub.callback']
end
def hub_lease_seconds
params['hub.lease_seconds']
end
def hub_secret
params['hub.secret']
end
def account_from_topic
if hub_topic.present? && local_domain? && account_feed_path?
Account.find_local(hub_topic_params[:username])
end
end
def hub_topic_params
@_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
end
def hub_topic_uri
@_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
end
def local_domain?
TagManager.instance.web_domain?(hub_topic_domain)
end
def verified_domain
return signed_request_account.domain if signed_request_account
end
def hub_topic_domain
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
end
def account_feed_path?
hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
class Api::SalmonController < Api::BaseController
include SignatureVerification
before_action :set_account
respond_to :txt
def update
if verify_payload?
process_salmon
head 202
elsif payload.present?
render plain: signature_verification_failure_reason, status: 401
else
head 400
end
end
private
def set_account
@account = Account.find(params[:id])
end
def payload
@_payload ||= request.body.read
end
def verify_payload?
payload.present? && VerifySalmonService.new.call(payload)
end
def process_salmon
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
end
end

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
class Api::SubscriptionsController < Api::BaseController
before_action :set_account
respond_to :txt
def show
if subscription.valid?(params['hub.topic'])
@account.update(subscription_expires_at: future_expires)
render plain: encoded_challenge, status: 200
else
head 404
end
end
def update
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
end
head 200
end
private
def subscription
@_subscription ||= @account.subscription(
api_subscription_url(@account.id)
)
end
def body
@_body ||= request.body.read
end
def encoded_challenge
HTMLEntities.new.encode(params['hub.challenge'])
end
def future_expires
Time.now.utc + lease_seconds_or_default
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 1.day).to_i.seconds
end
def set_account
@account = Account.find(params[:id])
end
end

View File

@ -28,14 +28,14 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses = statuses.without_replies if !@account.replies || truthy_param?(:exclude_replies)
statuses = statuses.without_reblogs if truthy_param?(:exclude_reblogs)
statuses = statuses.reblogs if truthy_param?(:reblogs) && !truthy_param?(:exclude_reblogs)
statuses = statuses.where(id: account_media_status_ids) if truthy_param?(:only_media)
statuses = statuses.hashtag_scope if params[:tagged].present?
statuses
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def permitted_account_statuses
@ -57,7 +57,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def pinned_scope
@account.pinned_statuses
if user_signed_in? && current_account.following?(@account)
@account.pinned_statuses
else
@account.pinned_statuses.where.not(visibility: :private)
end
end
def no_replies_scope
@ -72,7 +76,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
tag = Tag.find_normalized(params[:tagged])
if tag
Status.tagged_with(tag.id)
return Status.none if !user_signed_in && (tag.local || tag.private) || tag.private && current_account.id != @account.id
scope = tag.private ? current_account.statuses : tag.local ? Status.local : Status
scope.tagged_with(tag.id)
else
Status.none
end

View File

@ -23,6 +23,7 @@ class Api::V1::DomainBlocksController < Api::BaseController
def destroy
current_account.unblock_domain!(domain_block_params[:domain])
AfterAccountDomainUnblockWorker.perform_async(current_account.id, domain_block_params[:domain])
render_empty
end

View File

@ -43,6 +43,6 @@ class Api::V1::FiltersController < Api::BaseController
end
def resource_params
params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
params.permit(:phrase, :expires_in, :irreversible, :whole_word, :exclude_media, :media_only, :status_text, :spoiler, :tags, :custom_cw, :override_cw, context: [])
end
end

View File

@ -38,6 +38,6 @@ class Api::V1::ListsController < Api::BaseController
end
def list_params
params.permit(:title, :replies_policy)
params.permit(:title, :replies_policy, :show_self)
end
end

View File

@ -3,7 +3,7 @@
class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 20
RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 100).to_i
before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user!

View File

@ -30,10 +30,19 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
bookmark = Bookmark.find_or_create_by!(account: current_user.account, status: requested_status)
curate_status(requested_status)
bookmark.status.reload
end
def requested_status
Status.find(params[:status_id])
end
def curate_status(status)
return if status.curated || !status.distributable? || (status.reply? && status.in_reply_to_account_id != status.account_id)
status.curated = true
status.save
FanOutOnWriteService.new.call(status)
end
end

View File

@ -17,11 +17,12 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
private
def load_accounts
return [] if @status.local? && @status.account.user.setting_hide_interactions
default_accounts.merge(paginated_favourites).to_a
end
def default_accounts
Account
Account.without_unlisted
.includes(:favourites, :account_stat)
.references(:favourites)
.where(favourites: { status_id: @status.id })

View File

@ -17,11 +17,12 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
private
def load_accounts
return [] if @status.local? && @status.account.user.setting_hide_interactions
default_accounts.merge(paginated_statuses).to_a
end
def default_accounts
Account.includes(:statuses, :account_stat).references(:statuses)
Account.without_unlisted.includes(:statuses, :account_stat).references(:statuses)
end
def paginated_statuses

View File

@ -2,6 +2,7 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
include FilterHelper
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
@ -18,6 +19,8 @@ class Api::V1::StatusesController < Api::BaseController
def show
@status = cache_collection([@status], Status).first
# make sure any custom cws are applied
phrase_filtered?(@status, current_account.id, 'thread') unless current_account.nil?
render json: @status, serializer: REST::StatusSerializer
end
@ -52,12 +55,18 @@ class Api::V1::StatusesController < Api::BaseController
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
scheduled_at: status_params[:scheduled_at],
delete_after: status_params[:delete_after],
sharekey: status_params[:sharekey],
application: doorkeeper_token.application,
poll: status_params[:poll],
content_type: status_params[:content_type],
idempotency: request.headers['Idempotency-Key'])
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
if @status.nil?
raise Mastodon::ValidationError, 'Bangtags processed successfully.'
else
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
end
def destroy
@ -85,7 +94,9 @@ class Api::V1::StatusesController < Api::BaseController
:sensitive,
:spoiler_text,
:visibility,
:sharekey,
:scheduled_at,
:delete_after,
:content_type,
media_ids: [],
poll: [

View File

@ -28,6 +28,8 @@ class Api::V1::Timelines::TagController < Api::BaseController
def tagged_statuses
if @tag.nil?
[]
elsif @tag.name.in?(['self.bookmarks', '.self.bookmarks'])
Status.reorder(nil).joins(:bookmarks).merge(bookmark_results)
else
statuses = tag_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
@ -48,6 +50,18 @@ class Api::V1::Timelines::TagController < Api::BaseController
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
end
def bookmark_results
@_results ||= account_bookmarks.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def account_bookmarks
current_account.bookmarks
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end

View File

@ -8,6 +8,7 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_user_permissions, only: [:destroy]
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
prepend_before_action :switch_user
prepend_before_action :set_pack
before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes
@ -52,6 +53,10 @@ class Auth::SessionsController < Devise::SessionsController
params.require(:user).permit(:email, :password, :otp_attempt)
end
def switch_params
params.permit(:switch_to)
end
def after_sign_in_path_for(resource)
last_url = stored_location_for(:user)
@ -107,6 +112,19 @@ class Auth::SessionsController < Devise::SessionsController
render :two_factor
end
def switch_user
return unless switch_params[:switch_to].present? && current_user.present?
target_user = User.find_by(id: switch_params[:switch_to])
return unless target_user.present? && current_user.in?(target_user.linked_users)
self.resource = target_user
remember_me(target_user)
sign_in(target_user)
flash.delete(:error)
flash.delete(:alert)
flash.delete(:notice)
return root_path
end
private
def set_pack

View File

@ -11,6 +11,7 @@ module AccountControllerConcern
before_action :set_account
before_action :check_account_approval
before_action :check_account_suspension
before_action :check_account_hidden
before_action :set_instance_presenter
before_action :set_link_headers
end
@ -29,7 +30,6 @@ module AccountControllerConcern
response.headers['Link'] = LinkHeader.new(
[
webfinger_account_link,
atom_account_url_link,
actor_url_link,
]
)
@ -46,13 +46,6 @@ module AccountControllerConcern
]
end
def atom_account_url_link
[
account_url(@account, format: 'atom'),
[%w(rel alternate), %w(type application/atom+xml)],
]
end
def actor_url_link
[
ActivityPub::TagManager.instance.uri_for(@account),
@ -75,4 +68,8 @@ module AccountControllerConcern
gone
end
end
def check_account_hidden
not_found if @account.hidden?
end
end

View File

@ -2,8 +2,10 @@
module AccountableConcern
extend ActiveSupport::Concern
include LogHelper
def log_action(action, target)
Admin::ActionLog.create(account: current_account, action: action, target: target)
user_friendly_action_log(current_account, action, target)
end
end

View File

@ -145,7 +145,7 @@ module SignatureVerification
end
def account_refresh_key(account)
return if account.local? || !account.activitypub?
return if account.local?
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class DomainPolicyController < ApplicationController
before_action :authenticate_user!
before_action :set_pack
layout 'public'
before_action :set_instance_presenter, only: [:show]
def show
@hide_navbar = true
@domain_policies = DomainBlock.all.reorder('updated_at DESC').page(params[:page])
end
private
def set_pack
use_pack 'common'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def authenticate_user!
return if user_signed_in?
not_found
end
end

View File

@ -58,7 +58,7 @@ class FiltersController < ApplicationController
end
def resource_params
params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, :exclude_media, :media_only, :status_text, :spoiler, :tags, :custom_cw, :override_cw, context: [])
end
def set_body_classes

View File

@ -23,7 +23,7 @@ class HomeController < ApplicationController
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
if status && status.distributable?
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end

View File

@ -62,7 +62,7 @@ class RelationshipsController < ApplicationController
end
def dormant_account_scope
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(3.months.ago)))
end
def by_domain_scope

View File

@ -9,18 +9,10 @@ class RemoteFollowController < ApplicationController
before_action :set_body_classes
def new
@remote_follow = RemoteFollow.new(session_params)
end
raise Mastodon::NotPermittedError unless user_signed_in?
def create
@remote_follow = RemoteFollow.new(resource_params)
if @remote_follow.valid?
session[:remote_follow] = @remote_follow.acct
redirect_to @remote_follow.subscribe_address_for(@account)
else
render :new
end
FollowService.new.call(current_account, @account) unless current_account.following?(@account)
redirect_to TagManager.instance.url_for(@account)
end
private

View File

@ -5,24 +5,34 @@ class RemoteInteractionController < ApplicationController
layout 'modal'
before_action :set_interaction_type
before_action :set_status
before_action :set_body_classes
before_action :set_pack
before_action :set_status
def new
@remote_follow = RemoteFollow.new(session_params)
end
raise Mastodon::NotPermittedError unless user_signed_in?
def create
@remote_follow = RemoteFollow.new(resource_params)
if @remote_follow.valid?
session[:remote_follow] = @remote_follow.acct
redirect_to @remote_follow.interact_address_for(@status)
else
render :new
case params[:type]
when 'reblog'
if current_account.statuses.where(reblog: @status).exists?
status = current_account.statuses.find_by(reblog: @status)
RemoveStatusService.new.call(status)
else
ReblogService.new.call(current_account, @status)
end
when 'favourite'
if Favourite.where(account: current_account, status: @status).exists?
UnfavouriteService.new.call(current_account, @status)
else
FavouriteService.new.call(current_account, @status, skip_authorize: true)
end
when 'follow'
FollowService.new.call(current_account, @status.account)
when 'unfollow'
UnfollowService.new.call(current_account, @status.account)
end
redirect_to short_account_status_url(@status.account.username, @status.id, key: @sharekey)
end
private
@ -37,7 +47,13 @@ class RemoteInteractionController < ApplicationController
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
@sharekey = params[:key]
if @status.sharekey.present? && @sharekey == @status.sharekey
skip_authorization
else
authorize @status, :show?
end
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
@ -51,8 +67,4 @@ class RemoteInteractionController < ApplicationController
def set_pack
use_pack 'modal'
end
def set_interaction_type
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Settings
module Exports
class FollowersAccountsController < ApplicationController
include ExportControllerConcern
def index
send_export_file
end
private
def export_data
@export.to_followers_accounts_csv
end
end
end
end

View File

@ -26,6 +26,6 @@ class Settings::ImportsController < Settings::BaseController
end
def import_params
params.require(:import).permit(:data, :type)
params.require(:import).permit(:data, :type, :mode)
end
end

View File

@ -29,6 +29,39 @@ class Settings::PreferencesController < Settings::BaseController
def user_settings_params
params.require(:user).permit(
:setting_default_local,
:setting_always_local,
:setting_rawr_federated,
:setting_hide_stats,
:setting_hide_captions,
:setting_larger_menus,
:setting_larger_buttons,
:setting_larger_drawer,
:setting_larger_emoji,
:setting_remove_filtered,
:setting_hide_replies_muted,
:setting_hide_replies_blocked,
:setting_hide_replies_blocker,
:setting_hide_mntions_muted,
:setting_hide_mntions_blocked,
:setting_hide_mntions_blocker,
:setting_hide_mntions_packm8,
:setting_gently_kobolds,
:setting_user_is_kobold,
:setting_hide_mascot,
:setting_hide_interactions,
:setting_hide_public_profile,
:setting_hide_public_outbox,
:setting_max_public_history,
:setting_roar_lifespan,
:setting_delayed_roars,
:setting_delayed_for,
:setting_boost_interval,
:setting_boost_random,
:setting_boost_interval_from,
:setting_boost_interval_to,
:setting_show_cursor,
:setting_default_privacy,
:setting_default_sensitive,
:setting_default_language,

View File

@ -25,7 +25,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :replies, :locked, :hidden, :unlisted, :gently, :kobold, :adult_content, :bot, :discoverable, fields_attributes: [:name, :value])
end
def set_account

View File

@ -3,6 +3,7 @@
class StatusesController < ApplicationController
include SignatureAuthentication
include Authorization
include FilterHelper
ANCESTORS_LIMIT = 40
DESCENDANTS_LIMIT = 60
@ -12,6 +13,8 @@ class StatusesController < ApplicationController
before_action :set_account
before_action :set_status
before_action :handle_sharekey_change, only: [:show], if: :user_signed_in?
before_action :handle_webapp_redirect, only: [:show], if: :user_signed_in?
before_action :set_instance_presenter
before_action :set_link_headers
before_action :check_account_suspension
@ -179,7 +182,6 @@ class StatusesController < ApplicationController
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
]
)
@ -189,13 +191,40 @@ class StatusesController < ApplicationController
@status = @account.statuses.find(params[:id])
@stream_entry = @status.stream_entry
@type = @stream_entry.activity_type.downcase
@sharekey = params[:key]
authorize @status, :show?
# make sure any custom cws are applied
phrase_filtered?(@status, current_account.id, 'thread') unless current_account.nil?
if @status.sharekey.present? && @sharekey == @status.sharekey
skip_authorization
else
authorize @status, :show?
end
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404
raise ActiveRecord::RecordNotFound
end
def handle_sharekey_change
return if params[:rekey].nil?
raise Mastodon::NotPermittedError unless current_account.id == @status.account_id
case params[:rekey]
when '1'
@status.sharekey = SecureRandom.urlsafe_base64(32)
@status.save
Rails.cache.delete("statuses/#{@status.id}")
when '0'
@status.sharekey = nil
@status.save
Rails.cache.delete("statuses/#{@status.id}")
end
end
def handle_webapp_redirect
redirect_to "/web/statuses/#{@status.id}" if params[:toweb] == '1'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@ -12,6 +12,7 @@ class StreamEntriesController < ApplicationController
before_action :check_account_suspension
before_action :set_cache_headers
def show
respond_to do |format|
format.html do
@ -24,15 +25,6 @@ class StreamEntriesController < ApplicationController
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
end
format.atom do
unless @stream_entry.hidden?
skip_session!
expires_in 3.minutes, public: true
end
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
end
end
end
@ -49,7 +41,6 @@ class StreamEntriesController < ApplicationController
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
]
)

View File

@ -19,7 +19,7 @@ module Admin::ActionLogsHelper
elsif log.target_type == 'User' && [:change_email].include?(log.action)
log.recorded_changes.slice('email', 'unconfirmed_email')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
log.recorded_changes.slice('severity', 'reject_media', 'force_sensitive')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
end
@ -55,13 +55,13 @@ module Admin::ActionLogsHelper
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
when :enable, :allow_public, :allow_nonsensitive, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
'neutral'
when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
when :demote, :force_sensitive, :force_unlisted, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'
@ -87,7 +87,7 @@ module Admin::ActionLogsHelper
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
link_to record.domain, admin_instance_path(id: record.domain)
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
when 'AccountWarning'
@ -100,7 +100,7 @@ module Admin::ActionLogsHelper
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
link_to attributes['domain'], admin_instance_path(id: attributes['domain'])
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

View File

@ -0,0 +1,113 @@
module AutorejectHelper
include ModerationHelper
AUTOBLOCK_TRIGGERS = [:context, :context_starts_with, :context_contains]
def should_reject?(uri = nil)
if uri.nil?
if @object
uri = object_uri.start_with?('http') ? object_uri : @object['url']
elsif @json
uri = @json['id']
end
end
return if uri.nil?
domain = uri.scan(/[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*/).first
blocks = DomainBlock.suspend
return [:domain, uri] if blocks.where(domain: domain).or(blocks.where('domain LIKE ?', "%.#{domain}")).exists?
return unless @json || @object
context = @object['@context'] if @object
if @json
oid = @json['id']
if oid
return [:id_starts_with, uri] if ENV.fetch('REJECT_IF_ID_STARTS_WITH', '').split.any? { |r| oid.start_with?(r) }
return [:id_contains, uri] if ENV.fetch('REJECT_IF_ID_CONTAINS', '').split.any? { |r| r.in?(oid) }
end
username = @json['preferredUsername'] || @json['username']
if username && username.is_a?(String)
username = (@json['actor'] && @json['actor'].is_a?(String)) ? @json['actor'] : ''
username = username.scan(/(?<=\/user\/|\/@|\/users\/)([^\s\/]+)/).first
end
unless username.blank?
username.downcase!
return [:username, uri] if ENV.fetch('REJECT_IF_USERNAME_EQUALS', '').split.any? { |r| r == username }
return [:username_starts_with, uri] if ENV.fetch('REJECT_IF_USERNAME_STARTS_WITH', '').split.any? { |r| username.start_with?(r) }
return [:username_contains, uri] if ENV.fetch('REJECT_IF_USERNAME_CONTAINS', '').split.any? { |r| r.in?(username) }
end
context = @json['@context'] unless @object && context
end
return unless context
if context.is_a?(Array)
inline_context = context.find { |item| item.is_a?(Hash) }
if inline_context
keys = inline_context.keys
return [:context, uri] if ENV.fetch('REJECT_IF_CONTEXT_EQUALS', '').split.any? { |r| r.in?(keys) }
return [:context_starts_with, uri] if ENV.fetch('REJECT_IF_CONTEXT_STARTS_WITH', '').split.any? { |r| keys.any? { |k| k.start_with?(r) } }
return [:context_contains, uri] if ENV.fetch('REJECT_IF_CONTEXT_CONTAINS', '').split.any? { |r| keys.any? { |k| r.in?(k) } }
end
end
nil
end
def reject_reason(reason)
case reason
when :domain
"the origin domain is blocked"
when :id_starts_with
"the object's URI starts with a blocked phrase"
when :id_contains
"the object's URI contains a blocked phrase"
when :username
"the author's username is blocked"
when :username_starts_with
"the author's username starts with a blocked phrase"
when :username_contains
"the author's username contains a blocked phrase"
when :context
"the object's JSON-LD context has a key matching a blocked phrase"
when :context_starts_with
"the object's JSON-LD context has a key starting with a blocked phrase"
when :context_contains
"the object's JSON-LD context has a key containing a blocked phrase"
else
"of an undefined reason"
end
end
def should_autoblock?(reason)
@json['type'] == 'Create' && reason.in?(AUTOBLOCK_TRIGGERS)
end
def autoblock!(uri, reason)
return if uri.nil?
domain = uri.scan(/[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*/).first
domain_policy(uri, :suspend, "Sent an ActivityPub payload (#{uri}) where #{reason}.")
end
def autoreject?(uri = nil)
return false if @options && @options[:imported]
reason, uri = should_reject?(uri)
if reason
reason = reject_reason(reason)
if @json
autoblock!(uri, reason) if should_autoblock?(reason)
Rails.logger.info("Rejected an incoming '#{@json['type']}#{@object && " #{@object['type']}".rstrip}' from #{@json['id']} because #{reason}.")
elsif uri
Rails.logger.info("Rejected an outgoing request to #{uri} because #{reason}.")
end
return true
end
false
end
end

View File

@ -0,0 +1,58 @@
module BlocklistHelper
FEDIVERSE_SPACE_URLS = ["https://fediverse.network/mastodon?build=gab"]
VULPINE_CLUB_URL = "https://raw.githubusercontent.com/vulpineclub/vulpineclub.github.io/master/_data/blocks.yml"
def merged_blocklist
# ordered by preference
# prefer vulpine b/c they have easy-to-parse reason text
blocklist = vulpine_club_blocks | fediverse_space_blocks
blocklist.uniq { |entry| entry[:domain] }
end
def domain_map(domains, reason)
domains.map! do |domain|
{domain: domain, severity: :suspend, reason: reason}
end
end
def vulpine_club_blocks
body = Request.new(:get, VULPINE_CLUB_URL).perform do |response|
response.code != 200 ? nil : response.body_with_limit(66.kilobytes)
end
return [] unless body.present?
yaml = YAML::load(body)
yaml.map! do |entry|
domain = entry['domain']
next if domain.blank?
severity = entry['severity'].split('/')
reject_media = 'nomedia'.in?(severity)
severity = (severity[0].nil? || severity[0] == 'nomedia') ? 'noop' : severity[0]
reason = "Imported from <https://vulpine.club>: \"#{entry['reason']}\"#{entry['link'].present? ? " (#{entry['link']})" : ''}".rstrip
{domain: domain, severity: severity.to_sym, reject_media: reject_media, reason: reason}
end
end
# shamelessly adapted from @zac@computerfox.xyz's `silence` tool
# <https://github.com/theZacAttacks/silence/blob/master/silence>
# which you'll find useful if you're a non-monsterfork mastoadmin
def fediverse_space_fetch_domains(url)
body = Request.new(:get, url).perform do |response|
response.code != 200 ? nil : response.body_with_limit(66.kilobytes)
end
return [] unless body.present?
document = Nokogiri::HTML(body)
document.css('table.table-condensed td a').collect { |link| link.content.strip }
end
def fediverse_space_blocks
domains = FEDIVERSE_SPACE_URLS.flat_map { |url| fediverse_space_fetch_domains(url) }
domains.uniq!
domain_map(domains, "Imported from <https://fediverse.space>.")
end
end

View File

@ -0,0 +1,2 @@
module DomainPolicyHelper
end

View File

@ -0,0 +1,77 @@
module FilterHelper
include Redisable
def phrase_filtered?(status, receiver_id, context)
if redis.sismember("filtered_statuses:#{receiver_id}", status.id)
return !(redis.hexists("custom_cw:#{receiver_id}", status.id) || redis.hexists("custom_cw:#{receiver_id}", "c#{status.conversation_id}"))
end
filters = cached_filters(receiver_id)
filters.select! { |filter| filter.context.include?(context.to_s) && !filter.expired? }
if status.media_attachments.any?
filters.delete_if { |filter| filter.exclude_media }
else
filters.delete_if { |filter| filter.media_only }
end
return false if filters.empty?
status = status.reblog if status.reblog?
status_text = Formatter.instance.plaintext(status)
spoiler_text = status.spoiler_text
tags = status.tags.pluck(:name).join("\n")
filters.each do |filter|
if filter.whole_word
sb = filter.phrase =~ /\A[[:word:]]/ ? '\b' : ''
eb = filter.phrase =~ /[[:word:]]\z/ ? '\b' : ''
regex = /(?mix:#{sb}#{Regexp.escape(filter.phrase)}#{eb})/
else
regex = /#{Regexp.escape(filter.phrase)}/i
end
matched = false
matched = true unless regex.match(status_text).nil?
matched = true unless spoiler_text.blank? || regex.match(spoiler_text).nil?
matched = true unless tags.empty? || regex.match(tags).nil?
if matched
filter_thread(receiver_id, status.conversation_id) if filter.thread && filter.custom_cw.blank?
unless filter.custom_cw.blank?
cw = if filter.override_cw || status.spoiler_text.blank?
filter.custom_cw
else
"[#{filter.custom_cw}] #{status.spoiler_text}".rstrip
end
if filter.thread
redis.hset("custom_cw:#{receiver_id}", "c#{status.conversation_id}", cw)
else
redis.hset("custom_cw:#{receiver_id}", status.id, cw)
end
end
redis.sadd("filtered_statuses:#{receiver_id}", status.id)
return filter.custom_cw.blank?
end
end
false
end
def filter_thread(account_id, conversation_id)
return if Status.where(account_id: account_id, conversation_id: conversation_id).exists?
redis.sadd("filtered_threads:#{account_id}", conversation_id)
end
def filtering_thread?(account_id, conversation_id)
redis.sismember("filtered_threads:#{account_id}", conversation_id)
end
def cached_filters(account_id)
Rails.cache.fetch("filters:#{account_id}") { CustomFilter.where(account_id: account_id).to_a }.to_a
end
end

108
app/helpers/log_helper.rb Normal file
View File

@ -0,0 +1,108 @@
module LogHelper
def user_friendly_action_log(source, action, target, reason = nil)
source = source.username if source.is_a?(Account)
web_domain = Rails.configuration.x.web_domain || Rails.configuration.x.local_domain
case action
when :create
if target.is_a? DomainBlock
LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> applied a #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''} policy on '#{target.domain}'\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
elsif target.is_a? EmailDomainBlock
LogWorker.perform_async("\u26d4 <#{source}> added a registration block on email domain '#{target.domain}'.\n\nReview (moderators only): https://#{web_domain}/admin/email_domain_blocks")
elsif target.is_a? CustomEmoji
LogWorker.perform_async("\xf0\x9f\x98\xba <#{source}> added the '#{target.shortcode}' emoji. :#{target.shortcode}:")
elsif target.is_a? AccountWarning
LogWorker.perform_async("\xe2\x9a\xa0\xef\xb8\x8f <#{source}> sent someone an admin notice.")
end
when :destroy
if target.is_a? DomainBlock
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> reset the policy on #{target.domain}\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}")
elsif target.is_a? EmailDomainBlock
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> removed the registration block on email domain '#{target.domain}'.")
elsif target.is_a? CustomEmoji
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed the '#{target.shortcode}' emoji.")
elsif target.is_a? Status
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed post #{TagManager.instance.url_for(target)}\u200b.")
end
when :update
if target.is_a? DomainBlock
LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> changed the policy on '#{target.domain}' to #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''}.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
elsif target.is_a? Status
LogWorker.perform_async("\xf0\x9f\x91\x81\xef\xb8\x8f <#{source}> changed visibility flags of post #{TagManager.instance.url_for(target)}\u200b.")
elsif target.is_a? CustomEmoji
LogWorker.perform_async("\xf0\x9f\x94\x81 <#{source}> replaced the '#{target.shortcode}' emoji. :#{target.shortcode}:")
end
when :enable
if target.is_a? User
LogWorker.perform_async("\xf0\x9f\x92\xa7 <#{source}> unfroze the account of <#{target.username}>.")
elsif target.is_a? CustomEmoji
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> enabled the '#{target.shortcode}' emoji. :#{target.shortcode}:")
end
when :disable
if target.is_a? User
LogWorker.perform_async("\xe2\x9d\x84\xef\xb8\x8f <#{source}> froze the account of <#{target.username}>.")
elsif target.is_a? CustomEmoji
LogWorker.perform_async("\u26d4 <#{source}> disabled the '#{target.shortcode}' emoji.")
end
when :mark_unknown
if source.nil?
LogWorker.perform_async("\xf0\x9f\x86\x95 Federating with a new server at '#{target}'. Automatic reject unknown policy set.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target}")
else
LogWorker.perform_async("\u2753 <#{source}> marked <#{target.acct}> as an unknown account.\n\n#{reason ? "Comment: #{reason}" : ''}")
end
when :force_sensitive
LogWorker.perform_async("\xf0\x9f\x94\x9e <#{source}> forced the media of <#{target.acct}> to be marked sensitive.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :force_unlisted
LogWorker.perform_async("\xf0\x9f\x94\x89 <#{source}> forced the posts of <#{target.acct}> to be unlisted.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :silence
LogWorker.perform_async("\xf0\x9f\x94\x87 <#{source}> silenced <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :suspend
LogWorker.perform_async("\u26d4 <#{source}> suspended <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :mark_known
LogWorker.perform_async("\u2705 <#{source}> marked <#{target.acct}> as a known account.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :allow_nonsensitive
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> allowed <#{target.acct}> to post media without a sensitive flag.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :allow_public
LogWorker.perform_async("\xf0\x9f\x86\x8a <#{source}> allowed <#{target.acct}> to post with public visibility.")
when :unsilence
LogWorker.perform_async("\xf0\x9f\x94\x8a <#{source}> unsilenced <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :unsuspend
LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> unsuspended <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
when :remove_avatar
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed the avatar of <#{target.acct}>.")
when :remove_header
LogWorker.perform_async("\xf0\x9f\x97\x91\xef\xb8\x8f <#{source}> removed the profile header of <#{target.acct}>.")
when :resolve
LogWorker.perform_async("\u2705 <#{source}> resolved report #{target.id}.")
when :reopen
LogWorker.perform_async("\u2757 <#{source}> reopened report #{target.id}.")
when :assigned_to_self
LogWorker.perform_async("\xf0\x9f\x91\x80 <#{source}> is resolving report #{target.id}.")
when :unassigned
LogWorker.perform_async("\u274c <#{source}> is no longer assigned to report #{target.id}.")
when :promote
LogWorker.perform_async("\xf0\x9f\x94\xba <#{source}> upgraded a local account from #{target.role}.")
when :demote
LogWorker.perform_async("\xf0\x9f\x94\xbb <#{source}> downgraded a local account from #{target.role}.")
when :confirm
LogWorker.perform_async("\u2705 <#{source}> manually confirmed a local account.")
when :reset_password
LogWorker.perform_async("\xf0\x9f\x94\x81 <#{source}> manually reset a local account's password.")
when :disable_2fa
LogWorker.perform_async("\xf0\x9f\x94\x81 <#{source}> manually reset a local account's 2-factor auth.")
when :change_email
LogWorker.perform_async("\xf0\x9f\x93\x9d <#{source}> manually changed a local account's email address.")
when :memorialize
LogWorker.perform_async("\xf0\x9f\x8f\x85 <#{source}> memorialized an account.")
end
end
end

View File

@ -0,0 +1,125 @@
module ModerationHelper
include LogHelper
POLICIES = %w(silence unsilence suspend unsuspend force_unlisted mark_known mark_unknown reject_unknown allow_public force_sensitive allow_nonsensitive reset)
EXCLUDED_DOMAINS = %w(tailma.ws monsterpit.net monsterpit.cloud monsterpit.gallery monsterpit.blog)
def janitor_account
account_id = ENV.fetch('JANITOR_USER', '').to_i
return if account_id == 0
Account.find_by(id: account_id)
end
def account_policy(username, domain, policy, reason = nil)
return if policy.blank?
policy = policy.to_s
return false unless policy.in?(POLICIES)
username, domain = username.split('@')[1..2] if username.start_with?('@')
domain.downcase! unless domain.nil?
acct = Account.find_by(username: username, domain: domain)
return false if acct.nil?
if policy == 'reset'
Admin::ActionLog.create(account: @account, action: 'unsuspend', target: acct)
user_friendly_action_log(@account, :unsuspend, acct, reason)
else
Admin::ActionLog.create(account: @account, action: policy, target: acct)
user_friendly_action_log(@account, policy.to_sym, acct, reason)
end
case policy
when 'mark_unknown', 'reject_unknown'
acct.mark_unknown!
when 'mark_known'
acct.mark_known!
when 'silence'
acct.silence!
when 'unsilence'
acct.unsilence!
when 'suspend'
SuspendAccountService.new.call(acct, include_user: true)
return true
when 'unsuspend'
acct.unsuspend!
when 'force_unlisted'
acct.force_unlisted
when 'allow_public'
acct.allow_public!
when 'force_sensitive'
acct.force_sensitive!
when 'allow_nonsensitive'
acct.allow_nonsensitive!
when 'reset'
acct.unsuspend!
acct.unsilence!
acct.allow_public!
acct.allow_nonsensitive!
acct.mark_known!
end
acct.save
return true unless reason && !reason.strip.blank?
AccountModerationNote.create(
account_id: @account.id,
target_account_id: acct.id,
content: reason.strip
)
true
end
def domain_exists?(domain)
begin
code = Request.new(:head, "https://#{domain}").perform(&:code)
rescue
return false
end
return false if [404, 410].include?(code)
true
end
def domain_policy(domain, policy, reason = nil, force_sensitive: false, reject_unknown: false, reject_media: false, reject_reports: false)
return if policy.blank?
policy = policy.to_s
return false unless policy.in?(POLICIES)
return false unless domain.match?(/\A[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*\Z/)
domain.downcase!
return false if domain.in?(EXCLUDED_DOMAINS)
policy = 'noop' if policy == 'force_sensitive' || policy == 'reject_unknown'
force_sensitive = true if policy == 'force_sensitive'
reject_unknown = true if policy == 'reject_unknown'
if policy.in? %w(silence suspend force_unlisted)
return false unless domain_exists?(domain)
domain_block = DomainBlock.find_or_create_by(domain: domain)
domain_block.severity = policy
domain_block.force_sensitive = force_sensitive
domain_block.reject_unknown = reject_unknown
domain_block.reject_media = reject_media
domain_block.reject_reports = reject_reports
domain_block.reason = reason.strip if reason && !reason.strip.blank?
domain_block.save
Admin::ActionLog.create(account: @account, action: :create, target: domain_block)
user_friendly_action_log(@account, :create, domain_block)
DomainBlockWorker.perform_async(domain_block.id)
else
domain_block = DomainBlock.find_by(domain: domain)
return false if domain_block.nil?
Admin::ActionLog.create(account: @account, action: :destroy, target: domain_block)
user_friendly_action_log(@account, :destroy, domain_block)
DomainUnblockWorker.perform_async(domain_block.id)
end
true
end
end

View File

@ -68,7 +68,7 @@ module SettingsHelper
end
def filterable_languages
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
HUMAN_LOCALES.keys
end
def hash_to_object(hash)

View File

@ -35,18 +35,27 @@ module StreamEntriesHelper
end
def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do
content_tag(:div, class: 'roles') do
froze = account.local? ? (account&.user.nil? ? true : account.user.disabled?) : account.froze?
roles = []
roles << content_tag(:div, t('accounts.roles.froze'), class: 'account-role froze') if froze
roles << content_tag(:div, t('accounts.roles.locked'), class: 'account-role locked') if account.locked?
roles << content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot') if account.bot?
roles << content_tag(:div, t('accounts.roles.adult'), class: 'account-role adult') if account.adult_content?
roles << content_tag(:div, t('accounts.roles.gently'), class: 'account-role gently') if account.gently?
roles << content_tag(:div, t('accounts.roles.kobold'), class: 'account-role kobold') if account.kobold?
if (Setting.show_staff_badge && account.user_staff?) || all
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
roles << content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
roles << content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
roles << content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
roles.sum
end
end
@ -64,24 +73,33 @@ module StreamEntriesHelper
Setting.hide_followers_count || account.user&.setting_hide_followers_count
end
def hide_stats?(account)
Setting.hide_stats || account.user_hides_stats?
end
def account_description(account)
prepend_stats = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
]
if hide_stats?(account)
prepend_stats = []
else
prepend_stats = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),
unless hide_followers_count?(account)
prepend_stats << [
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' ')
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
]
unless hide_followers_count?(account)
prepend_stats << [
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' ')
end
end
[prepend_stats.join(', '), account.note].join(' · ')
@ -187,6 +205,8 @@ module StreamEntriesHelper
fa_icon 'globe fw'
when 'unlisted'
fa_icon 'unlock fw'
when 'local'
fa_icon 'users fw'
when 'private'
fa_icon 'lock fw'
when 'direct'

38
app/helpers/url_helper.rb Normal file
View File

@ -0,0 +1,38 @@
module UrlHelper
def sanitize_query_string(url)
return if url.blank?
url = Addressable::URI.parse(url)
return url.to_s if url.query.blank?
return unless '='.in?(url.query)
params = CGI.parse(url.query)
params.delete_if do |key|
k = key.downcase
next true if k.start_with?(
'_hs',
'ic',
'mc_',
'mkt_',
'ns_',
'sr_',
'utm',
'vero_',
'nr_',
'ref',
)
next true if 'track'.in?(k)
next true if [
'fbclid',
'gclid',
'ncid',
'ocid',
'r',
'spm',
].include?(k)
false
end
url.query_values = params
return url.to_s
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
return '#'
end
end

View File

@ -144,7 +144,9 @@ export function submitCompose(routerHistory) {
dispatch(submitComposeRequest());
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';
if (!/(?:#|&num;|&#35;)(?:!|&excl;|&#33;)(?:<\/p>)?$/.test(status)) {
status = status + ' #!';
}
}
api(getState).post('/api/v1/statuses', {
status,
@ -227,7 +229,7 @@ export function doodleSet(options) {
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = 4;
const uploadLimit = 6;
const media = getState().getIn(['compose', 'media_attachments']);
const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
@ -245,7 +247,7 @@ export function uploadCompose(files) {
dispatch(uploadComposeRequest());
for (const [i, f] of Array.from(files).entries()) {
if (media.size + i > 3) break;
if (media.size + i > 5) break;
resizeImage(f).then(file => {
const data = new FormData();

View File

@ -22,7 +22,7 @@ export function normalizeAccount(account) {
if (account.fields) {
account.fields = account.fields.map(pair => ({
...pair,
name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap),
value_emojified: emojify(pair.value, emojiMap),
value_plain: unescapeHTML(pair.value),
}));

View File

@ -150,10 +150,10 @@ export const createListFail = error => ({
error,
});
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
export const updateList = (id, title, shouldReset, replies_policy, show_self) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, show_self }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {

View File

@ -37,7 +37,7 @@ export function submitSearch() {
params: {
q: value,
resolve: true,
limit: 10,
limit: 33,
},
}).then(response => {
if (response.data.accounts) {

View File

@ -9,6 +9,7 @@ import {
import { updateNotifications, expandNotifications } from './notifications';
import { fetchFilters } from './filters';
import { getLocale } from 'mastodon/locales';
import { resetCompose } from 'flavours/glitch/actions/compose';
const { messages } = getLocale();
@ -40,6 +41,14 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
case 'filters_changed':
dispatch(fetchFilters());
break;
case 'switch_accounts':
dispatch(resetCompose());
window.location.href = `/auth/sign_in?switch_to=${data.payload}`
break;
case 'refresh':
dispatch(resetCompose());
window.location.reload();
break;
}
},
};

View File

@ -95,7 +95,7 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountTimeline = (accountId, { maxId, withReplies, reblogs } = {}) => expandTimeline(`account:${accountId}${reblogs ? ':reblogs' : withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, reblogs: reblogs, exclude_reblogs: !reblogs, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

View File

@ -1,30 +1,13 @@
import React from 'react';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink';
import { shortNumberFormat } from 'flavours/glitch/util/numbers';
const Hashtag = ({ hashtag }) => (
<div className='trends__item'>
<div className='trends__item__name'>
<Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
#<span>{hashtag.get('name')}</span>
</Permalink>
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} />
</div>
<div className='trends__item__current'>
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))}
</div>
<div className='trends__item__sparkline'>
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</div>
</div>
<Permalink className='hashtag' href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
#<span>{hashtag.get('name')}</span>
</Permalink>
);
Hashtag.propTypes = {

View File

@ -58,6 +58,9 @@ class Item extends React.PureComponent {
handleMouseEnter = (e) => {
if (this.hoverToPlay()) {
e.target.play();
} else if (this.hoverToPlayClassicGif()) {
const { attachment } = this.props;
e.target.src = attachment.get('url');
}
}
@ -65,6 +68,9 @@ class Item extends React.PureComponent {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
} else if (this.hoverToPlayClassicGif()) {
const { attachment } = this.props;
e.target.src = attachment.get('preview_url');
}
}
@ -73,6 +79,14 @@ class Item extends React.PureComponent {
return !autoPlayGif && attachment.get('type') === 'gifv';
}
hoverToPlayClassicGif () {
const { attachment } = this.props;
return !autoPlayGif && (
attachment.get('type') === 'image' &&
attachment.get('url').split('.').pop().startsWith('gif')
);
}
handleClick = (e) => {
const { index, onClick } = this.props;
@ -80,6 +94,9 @@ class Item extends React.PureComponent {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
} else if (this.hoverToPlayClassicGif()) {
const { attachment } = this.props;
e.target.src = attachment.get('preview_url');
}
e.preventDefault();
onClick(index);
@ -124,54 +141,12 @@ class Item extends React.PureComponent {
const { attachment, index, size, standalone, letterbox, displayWidth, visible } = this.props;
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
let height = 100 / Math.ceil(size/2);
if (size === 1) {
if (size === 1 || size % 2 == 1 && index == 0) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (index === 0) {
right = '2px';
} else if (index > 0) {
left = '2px';
}
if (index === 1) {
bottom = '2px';
} else if (index > 1) {
top = '2px';
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '2px';
}
if (index === 1 || index === 3) {
left = '2px';
}
if (index < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
let thumbnail = '';
if (attachment.get('type') === 'unknown') {
@ -199,25 +174,54 @@ class Item extends React.PureComponent {
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
thumbnail = (
<a
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
>
<img
className={letterbox ? 'letterbox' : null}
src={previewUrl}
srcSet={srcSet}
sizes={sizes}
alt={attachment.get('description')}
title={attachment.get('description')}
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
</a>
);
const isGif = originalUrl.split('.').pop().startsWith('gif');
const autoPlay = !isIOS() && autoPlayGif;
if (isGif && !autoPlay) {
thumbnail = (
<a
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
>
<img
className={letterbox ? 'letterbox' : null}
src={previewUrl}
sizes={sizes}
alt={attachment.get('description')}
title={attachment.get('description')}
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onMouseDown={this.handleMouseDown}
onLoad={this.handleImageLoad}
/>
<span className='media-gallery__gifv__label'>GIF</span>
</a>
);
} else {
thumbnail = (
<a
className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick}
target='_blank'
>
<img
className={letterbox ? 'letterbox' : null}
src={previewUrl}
srcSet={srcSet}
sizes={sizes}
alt={attachment.get('description')}
title={attachment.get('description')}
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
</a>
);
}
} else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && autoPlayGif;
@ -243,7 +247,7 @@ class Item extends React.PureComponent {
}
return (
<div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ width: `${width}%`, height: `${height}%` }}>
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
{visible && thumbnail}
</div>
@ -320,7 +324,7 @@ export default class MediaGallery extends React.PureComponent {
render () {
const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props;
const { visible } = this.state;
const size = media.take(4).size;
const size = media.take(6).size;
const width = this.state.width || defaultWidth;
@ -341,7 +345,7 @@ export default class MediaGallery extends React.PureComponent {
if (this.isStandaloneEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />);
children = media.take(6).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />);
}
if (visible) {
@ -354,19 +358,75 @@ export default class MediaGallery extends React.PureComponent {
);
}
let parts = {};
media.map(
(attachment, i) => {
if (attachment.get('description')) {
if (attachment.get('description') in parts) {
parts[attachment.get('description')].push([i, attachment.get('url'), attachment.get('id')]);
} else {
parts[attachment.get('description')] = [[i, attachment.get('url'), attachment.get('id')]];
}
}
}
);
let descriptions = Object.entries(parts).map(
part => {
let [desc, idx] = part;
if (idx.length == 1) {
let url = idx[0][1];
return (
<p key={idx[0][2]}>
<strong>
<a href={url} title={url} target='_blank' rel='nofollow noopener'>
Attachment #{1+idx[0][0]}
</a>
</strong>
{': '} {desc}
</p>
);
} else if (idx.length != 0) {
let c=0;
return (
<p key={idx[0][2]}>
<strong>
Attachments
{
idx.map(i => {
let url = i[1];
c++;
return (<span key={i[2]}>{c == 1 ? ' ' : ', '}<a href={url} title={url} target='_blank' rel='nofollow noopener'>#{1+i[0]}</a></span>);
})
}
</strong>
{': '} {desc}
</p>
);
}
}
);
return (
<div className={computedClass} style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
{spoilerButton}
{visible && sensitive && (
<span className='sensitive-marker'>
<FormattedMessage {...messages.sensitive} />
</span>
)}
<React.Fragment>
<div className={computedClass} style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
{spoilerButton}
{visible && sensitive && (
<span className='sensitive-marker'>
<FormattedMessage {...messages.sensitive} />
</span>
)}
</div>
{children}
</div>
{children}
</div>
<div className='media-caption'>
{descriptions}
</div>
</React.Fragment>
);
}

View File

@ -440,7 +440,6 @@ export default class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div>
</HotKeys>
);

View File

@ -192,11 +192,12 @@ export default class StatusActionBar extends ImmutablePureComponent {
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const reblogDisabled = status.get('visibility') === 'direct' || (status.get('visibility') === 'private' && me !== status.getIn(['account', 'id']));
const reblogMessage = status.get('visibility') === 'private' ? messages.reblog_private : messages.reblog;
let menu = [];
let reblogIcon = 'retweet';
let reblogIcon = 'repeat';
let replyIcon;
let replyTitle;
@ -209,13 +210,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
menu.push(null);
if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
if (status.getIn(['account', 'id']) === me) {
if (publicStatus) {
if (pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}

View File

@ -59,6 +59,12 @@ export default class StatusIcons extends React.PureComponent {
aria-hidden='true'
/>
) : null}
{status.get('delete_after') ? (
<i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} aria-hidden='true' />
) : null}
{status.get('reject_replies') ? (
<i className='fa fa-microphone-slash' title='Rejecting replies' aria-hidden='true' />
) : null}
{(
<VisibilityIcon visibility={status.get('visibility')} />
)}

View File

@ -82,7 +82,7 @@ export default class StatusPrepend extends React.PureComponent {
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
<i
className={`fa fa-fw fa-${
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet'))
type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'repeat'))
} status__prepend-icon`}
/>
</div>

View File

@ -6,6 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
local: { id: 'privacy.local.short', defaultMessage: 'Community' },
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
@ -25,6 +26,7 @@ export default class VisibilityIcon extends ImmutablePureComponent {
const visibilityClass = {
public: 'globe',
local: 'users',
unlisted: 'unlock',
private: 'lock',
direct: 'envelope',

View File

@ -49,19 +49,16 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'>
<div className='account__action-bar-links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' title={account.get('statuses_count')} to={`/accounts/${account.get('id')}`}>
<FormattedMessage id='account.posts' defaultMessage='Posts' />
<strong><FormattedNumber value={account.get('statuses_count')} /></strong>
</NavLink>
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
<NavLink exact activeClassName='active' className='account__action-bar__tab' title={account.get('following_count')} to={`/accounts/${account.get('id')}/following`}>
<FormattedMessage id='account.follows' defaultMessage='Follows' />
<strong><FormattedNumber value={account.get('following_count')} /></strong>
</NavLink>
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
<NavLink exact activeClassName='active' className='account__action-bar__tab' title={account.get('followers_count') < 0 ? '(hidden)' : account.get('followers_count')} to={`/accounts/${account.get('id')}/followers`}>
<FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
</NavLink>
</div>
</div>

View File

@ -80,7 +80,6 @@ class Header extends ImmutablePureComponent {
let info = [];
let actionBtn = '';
let lockedIcon = '';
let menu = [];
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
@ -114,10 +113,6 @@ class Header extends ImmutablePureComponent {
actionBtn = '';
}
if (account.get('locked')) {
lockedIcon = <Icon icon='lock' title={intl.formatMessage(messages.account_locked)} />;
}
if (account.get('id') !== me) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
@ -189,7 +184,15 @@ class Header extends ImmutablePureComponent {
const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
const badge_locked = account.get('locked') ? (<div className='account-role locked'><FormattedMessage id='account.badges.locked' defaultMessage='🔒 Locked' /></div>) : null;
const badge_froze = account.get('froze') ? (<div className='account-role froze'><FormattedMessage id='account.badges.froze' defaultMessage='❄️ Frozen by admin' /></div>) : null;
const badge_bot = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
const badge_ac = account.get('adult_content') ? (<div className='account-role adult'><FormattedMessage id='account.badges.adult' defaultMessage="🔞 Adult content" /></div>) : null;
const badge_gently = account.get('gently') ? (<div className='account-role gently'><FormattedMessage id='account.badges.gently' defaultMessage="Gentlies kobolds" /></div>) : null;
const badge_kobold = account.get('kobold') ? (<div className='account-role kobold'><FormattedMessage id='account.badges.kobold' defaultMessage="Gently the kobold" /></div>) : null;
const badge_mod = account.get('role') == 'moderator' ? (<div className='account-role moderator'><FormattedMessage id='account.badges.moderator' defaultMessage="Moderator" /></div>) : null;
const badge_admin = account.get('role') == 'admin' ? (<div className='account-role admin'><FormattedMessage id='account.badges.admin' defaultMessage="Admin" /></div>) : null;
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
return (
@ -219,8 +222,9 @@ class Header extends ImmutablePureComponent {
<div className='account__header__tabs__name'>
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
<small>@{acct} {lockedIcon}</small>
<span dangerouslySetInnerHTML={displayNameHtml} />
<small>@{acct}</small>
<div className='roles'>{badge_admin}{badge_mod}{badge_froze}{badge_locked}{badge_ac}{badge_bot}{badge_gently}{badge_kobold}</div>
</h1>
</div>
@ -243,7 +247,7 @@ class Header extends ImmutablePureComponent {
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>

View File

@ -119,7 +119,8 @@ export default class Header extends ImmutablePureComponent {
{!hideTabs && (
<div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
{account.get('replies') && (<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>)}
<NavLink exact to={`/accounts/${account.get('id')}/reblogs`}><FormattedMessage id='account.reblogs' defaultMessage='Boosts' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
)}

View File

@ -15,13 +15,13 @@ import { FormattedMessage } from 'react-intl';
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
const path = withReplies ? `${accountId}:with_replies` : accountId;
const mapStateToProps = (state, { params: { accountId }, withReplies = false, reblogs = false }) => {
const path = reblogs ? `${accountId}:reblogs` : withReplies ? `${accountId}:with_replies` : accountId;
return {
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
featuredStatusIds: (withReplies || reblogs) ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
};
@ -38,28 +38,29 @@ export default class AccountTimeline extends ImmutablePureComponent {
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
reblogs: PropTypes.bool,
isAccount: PropTypes.bool,
};
componentWillMount () {
const { params: { accountId }, withReplies } = this.props;
const { params: { accountId }, withReplies, reblogs } = this.props;
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountIdentityProofs(accountId));
if (!withReplies) {
if (!withReplies && !reblogs) {
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
}
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
this.props.dispatch(expandAccountTimeline(accountId, { withReplies: withReplies, reblogs: reblogs }));
}
componentWillReceiveProps (nextProps) {
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies || nextProps.reblogs !== this.props.reblogs) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
if (!nextProps.withReplies) {
if (!nextProps.withReplies && !nextProps.reblogs) {
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
}
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies, reblogs: nextProps.params.reblogs }));
}
}
@ -68,7 +69,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
}
handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies, reblogs: this.props.reblogs }));
}
setRef = c => {

View File

@ -17,7 +17,12 @@ import Publisher from './publisher';
import TextareaIcons from './textarea_icons';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'Roar shamelessly!' },
placeholder_as: {
id: 'compose_form.placeholder_as',
defaultMessage: "Signing as {nickname}.\nRoar shamelessly!",
values: {nickname: 'yourself'}
},
missingDescriptionMessage: { id: 'confirmations.missing_media_description.message',
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm',
@ -47,6 +52,7 @@ class ComposeForm extends ImmutablePureComponent {
isUploading: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
onClearAll: PropTypes.func,
onClearSuggestions: PropTypes.func,
onFetchSuggestions: PropTypes.func,
onSuggestionSelected: PropTypes.func,
@ -70,6 +76,7 @@ class ComposeForm extends ImmutablePureComponent {
onUnmount: PropTypes.func,
onPaste: PropTypes.func,
onMediaDescriptionConfirm: PropTypes.func,
account: ImmutablePropTypes.map.isRequired,
};
static defaultProps = {
@ -105,6 +112,7 @@ class ComposeForm extends ImmutablePureComponent {
text,
mediaDescriptionConfirmation,
onMediaDescriptionConfirm,
onClearAll,
} = this.props;
// If something changes inside the textarea, then we update the
@ -162,6 +170,10 @@ class ComposeForm extends ImmutablePureComponent {
this.handleSubmit();
}
handleClearAll = () => {
this.props.onClearAll();
}
// Selects a suggestion from the autofill.
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
@ -273,6 +285,7 @@ class ComposeForm extends ImmutablePureComponent {
handleSelect,
handleSubmit,
handleRefTextarea,
handleClearAll,
} = this;
const {
advancedOptions,
@ -297,9 +310,11 @@ class ComposeForm extends ImmutablePureComponent {
suggestions,
text,
spoilersAlwaysOn,
account,
} = this.props;
let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
let nickname = (this.props.account !== undefined) ? this.props.account.get('identity') : '';
return (
<div className='composer'>
@ -331,7 +346,7 @@ class ComposeForm extends ImmutablePureComponent {
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
placeholder={nickname ? intl.formatMessage(messages.placeholder_as, {nickname: nickname}) : intl.formatMessage(messages.placeholder)}
disabled={isSubmitting}
value={this.props.text}
onChange={this.handleChange}
@ -368,6 +383,7 @@ class ComposeForm extends ImmutablePureComponent {
disabled={disabledButton}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
onClearAll={handleClearAll}
privacy={privacy}
sideArm={sideArm}
/>

View File

@ -25,6 +25,22 @@ const messages = defineMessages({
defaultMessage: 'Attach...',
id: 'compose.attach',
},
bbcode: {
defaultMessage: 'BBCode',
id: 'compose.content-type.bbcode',
},
bbdown: {
defaultMessage: 'BBdown',
id: 'compose.content-type.bbdown',
},
local_short: {
defaultMessage: 'Community',
id: 'privacy.local.short'
},
local_long: {
defaultMessage: 'Post to community timeline',
id: 'privacy.local.long'
},
change_privacy: {
defaultMessage: 'Adjust status privacy',
id: 'privacy.change',
@ -61,6 +77,10 @@ const messages = defineMessages({
defaultMessage: 'Markdown',
id: 'compose.content-type.markdown',
},
console: {
defaultMessage: 'Console',
id: 'compose.content-type.console',
},
plain: {
defaultMessage: 'Plain text',
id: 'compose.content-type.plain',
@ -228,24 +248,45 @@ class ComposerOptions extends ImmutablePureComponent {
name: 'unlisted',
text: <FormattedMessage {...messages.unlisted_short} />,
},
local: {
icon: 'users',
meta: <FormattedMessage {...messages.local_long} />,
name: 'local',
text: <FormattedMessage {...messages.local_short} />,
}
};
const contentTypeItems = {
plain: {
icon: 'align-left',
icon: 'file-text',
name: 'text/plain',
text: <FormattedMessage {...messages.plain} />,
},
console: {
icon: 'terminal',
name: 'text/console',
text: <FormattedMessage {...messages.console} />,
},
html: {
icon: 'code',
name: 'text/html',
text: <FormattedMessage {...messages.html} />,
},
markdown: {
icon: 'arrow-circle-down',
icon: 'hashtag',
name: 'text/markdown',
text: <FormattedMessage {...messages.markdown} />,
},
xbbcode: {
icon: 'thumb-tack',
name: 'text/x-bbcode',
text: <FormattedMessage {...messages.bbcode} />,
},
xbbcodemarkdown: {
icon: 'arrow-circle-down',
name: 'text/x-bbcode+markdown',
text: <FormattedMessage {...messages.bbdown} />,
},
};
// The result.
@ -302,6 +343,7 @@ class ComposerOptions extends ImmutablePureComponent {
icon={(privacyItems[privacy] || {}).icon}
items={[
privacyItems.public,
privacyItems.local,
privacyItems.unlisted,
privacyItems.private,
privacyItems.direct,
@ -315,11 +357,14 @@ class ComposerOptions extends ImmutablePureComponent {
{showContentTypeChoice && (
<Dropdown
disabled={disabled}
icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
icon={(contentTypeItems[contentType.split('/')[1].replace(/[+-]/g, '')] || {}).icon}
items={[
contentTypeItems.plain,
contentTypeItems.html,
contentTypeItems.xbbcodemarkdown,
contentTypeItems.markdown,
contentTypeItems.xbbcode,
contentTypeItems.html,
contentTypeItems.plain,
contentTypeItems.console,
]}
onChange={onChangeContentType}
onModalClose={onModalClose}

View File

@ -144,6 +144,7 @@ class PollForm extends ImmutablePureComponent {
</select>
<select value={expiresIn} onChange={this.handleSelectDuration}>
<option value={60}>{intl.formatMessage(messages.minutes, { number: 1 })}</option>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
@ -151,6 +152,10 @@ class PollForm extends ImmutablePureComponent {
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
<option value={1209600}>{intl.formatMessage(messages.days, { number: 14 })}</option>
<option value={2592000}>{intl.formatMessage(messages.days, { number: 30 })}</option>
<option value={5184000}>{intl.formatMessage(messages.days, { number: 60 })}</option>
<option value={7776000}>{intl.formatMessage(messages.days, { number: 90 })}</option>
</select>
</div>
</div>

View File

@ -23,6 +23,10 @@ const messages = defineMessages({
defaultMessage: '{publish}!',
id: 'compose_form.publish_loud',
},
clear: {
defaultMessage: 'Clear',
id: 'compose_form.clear',
},
});
export default @injectIntl
@ -34,12 +38,13 @@ class Publisher extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
onSecondarySubmit: PropTypes.func,
onSubmit: PropTypes.func,
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
onClearAll: PropTypes.func,
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'local', 'public']),
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'local', 'public']),
};
render () {
const { countText, disabled, intl, onSecondarySubmit, onSubmit, privacy, sideArm } = this.props;
const { countText, disabled, intl, onSecondarySubmit, onSubmit, onClearAll, privacy, sideArm } = this.props;
const diff = maxChars - length(countText || '');
const computedClass = classNames('composer--publisher', {
@ -49,6 +54,17 @@ class Publisher extends ImmutablePureComponent {
return (
<div className={computedClass}>
<Button
className='clear'
onClick={onClearAll}
title={intl.formatMessage(messages.clear)}
disabled={disabled || diff < 0}
text={
<span>
<Icon icon='trash-o' />
</span>
}
/>
<span className='count'>{diff}</span>
{sideArm && sideArm !== 'none' ? (
<Button
@ -61,6 +77,7 @@ class Publisher extends ImmutablePureComponent {
<Icon
icon={{
public: 'globe',
local: 'users',
unlisted: 'unlock',
private: 'lock',
direct: 'envelope',
@ -86,6 +103,7 @@ class Publisher extends ImmutablePureComponent {
private: 'lock',
public: 'globe',
unlisted: 'unlock',
local: 'users',
}[privacy]}
/>
{' '}

View File

@ -47,7 +47,7 @@ class ReplyIndicator extends ImmutablePureComponent {
}
const account = status.get('account');
const content = status.get('content');
const content = status.get('contentHtml');
const attachments = status.get('media_attachments');
// The result.

View File

@ -84,7 +84,9 @@ class SearchResults extends ImmutablePureComponent {
<section>
<h5><Icon icon='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
<div className='hashtags'>
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>
</section>
);
}
@ -98,8 +100,8 @@ class SearchResults extends ImmutablePureComponent {
</header>
{accounts}
{statuses}
{hashtags}
{statuses}
</div>
);
};

View File

@ -14,6 +14,7 @@ import {
submitCompose,
unmountCompose,
uploadCompose,
resetCompose,
} from 'flavours/glitch/actions/compose';
import {
openModal,
@ -22,6 +23,8 @@ import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
import { me } from 'flavours/glitch/util/initial_state';
const messages = defineMessages({
missingDescriptionMessage: { id: 'confirmations.missing_media_description.message',
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
@ -68,6 +71,7 @@ function mapStateToProps (state) {
spoilersAlwaysOn: spoilersAlwaysOn,
mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
account: state.getIn(['accounts', me]),
};
};
@ -131,6 +135,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}));
},
onClearAll() {
dispatch(resetCompose());
}
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeForm));

View File

@ -16,7 +16,7 @@ function mapStateToProps (state) {
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
resetFileKey: state.getIn(['compose', 'resetFileKey']),
hasPoll: !!poll,
allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true),
allowMedia: !poll && (media ? media.size < 6 && !media.some(item => item.get('type') === 'video') : true),
hasMedia: media && !!media.size,
allowPoll: !(media && !!media.size),
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),

View File

@ -5,23 +5,16 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { me } from 'flavours/glitch/util/initial_state';
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
});
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
const WarningWrapper = ({ needsLockWarning, directMessageWarning }) => {
if (needsLockWarning) {
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
}
if (hashtagWarning) {
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag." />} />;
}
if (directMessageWarning) {
const message = (
<span>
@ -37,7 +30,6 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
WarningWrapper.propTypes = {
needsLockWarning: PropTypes.bool,
hashtagWarning: PropTypes.bool,
directMessageWarning: PropTypes.bool,
};

View File

@ -10,7 +10,7 @@ import SearchContainer from './containers/search_container';
import Motion from 'flavours/glitch/util/optional_motion';
import spring from 'react-motion/lib/spring';
import SearchResultsContainer from './containers/search_results_container';
import { me, mascot } from 'flavours/glitch/util/initial_state';
import { me, mascot, isStaff } from 'flavours/glitch/util/initial_state';
import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
import HeaderContainer from './containers/header_container';
@ -62,6 +62,20 @@ class Compose extends React.PureComponent {
{!isSearchPage && <div className='drawer__inner'>
<NavigationContainer />
<ComposeFormContainer />
{isStaff && multiColumn && (
<div className='drawer__inner__admin'>
<h2>Staff Tools</h2>
<ul>
<li><a href="/admin/action_logs" target="_blank" rel="nofollow noopener">Audit log</a></li>
<li><a href="/admin/reports" target="_blank" rel="nofollow noopener">Reports</a></li>
<li><a href="/admin/pending_accounts" target="_blank" rel="nofollow noopener">Pending accounts</a></li>
<li><a href="/admin/domain_blocks/new" target="_blank" rel="nofollow noopener">Add domain policy...</a></li>
<li><a href="/admin/instances" target="_blank" rel="nofollow noopener">Federation</a></li>
<li><a href="/admin/accounts" target="_blank" rel="nofollow noopener">Accounts</a></li>
<li><a href="/admin/custom_emojis" target="_blank" rel="nofollow noopener">Custom emojis</a></li>
</ul>
</div>
)}
{multiColumn && (
<div className='drawer__inner__mastodon'>
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}

View File

@ -13,10 +13,12 @@ import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists
import { openModal } from 'flavours/glitch/actions/modal';
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import Toggle from 'react-toggle';
const messages = defineMessages({
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
show_self: { id: 'lists.show_self', defaultMessage: 'Include your own toots' },
all_replies: { id: 'lists.replies_policy.all_replies', defaultMessage: 'any followed user' },
no_replies: { id: 'lists.replies_policy.no_replies', defaultMessage: 'no one' },
list_replies: { id: 'lists.replies_policy.list_replies', defaultMessage: 'members of the list' },
@ -114,6 +116,14 @@ export default class ListTimeline extends React.PureComponent {
}));
}
handleShowSelfChange = ({ target }) => {
const { dispatch, list } = this.props;
const { id } = this.props.params;
const replies_policy = list ? list.get('replies_policy') : undefined;
const show_self = list ? list.get('show_self') : false;
this.props.dispatch(updateList(id, undefined, false, replies_policy, !show_self));
}
handleRepliesPolicyChange = ({ target }) => {
const { dispatch, list } = this.props;
const { id } = this.props.params;
@ -126,6 +136,7 @@ export default class ListTimeline extends React.PureComponent {
const pinned = !!columnId;
const title = list ? list.get('title') : id;
const replies_policy = list ? list.get('replies_policy') : undefined;
const show_self = list ? list.get('show_self') : false;
if (typeof list === 'undefined') {
return (
@ -167,13 +178,22 @@ export default class ListTimeline extends React.PureComponent {
</button>
</div>
<div className='column-settings__row'>
<div className='setting-toggle'>
<Toggle id={['setting', 'toggle', id, 'show_self'].join('-')} checked={show_self === true} onChange={this.handleShowSelfChange} />
<label htmlFor={['setting', 'toggle', id, 'show_self'].join('-')} className='setting-toggle__label'>
<FormattedMessage id='lists.show_self' defaultMessage='Include your own toots' />
</label>
</div>
</div>
{ replies_policy !== undefined && (
<div>
<div className='column-settings__row'>
<fieldset>
<legend><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></legend>
{ ['no_replies', 'list_replies', 'all_replies'].map(policy => (
<div className='setting-radio'>
<div key={['setting', 'radio', id, policy].join('-')} className='setting-radio'>
<input className='setting-radio__input' id={['setting', 'radio', id, policy].join('-')} type='radio' value={policy} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
<label className='setting-radio__label' htmlFor={['setting', 'radio', id, policy].join('-')}>
<FormattedMessage {...messages[policy]} />

View File

@ -81,11 +81,13 @@ export default class NotificationFollow extends ImmutablePureComponent {
<i className='fa fa-fw fa-user-plus' />
</div>
<FormattedMessage
id='notification.follow'
defaultMessage='{name} followed you'
values={{ name: link }}
/>
<span title={notification.get('created_at')}>
<FormattedMessage
id='notification.follow'
defaultMessage='{name} followed you'
values={{ name: link }}
/>
</span>
</div>
<AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />

View File

@ -67,7 +67,7 @@ export default class Reblogs extends ImmutablePureComponent {
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='retweet'
icon='repeat'
title={intl.formatMessage(messages.heading)}
onClick={this.handleHeaderClick}
showBackButton

View File

@ -143,6 +143,7 @@ export default class ActionBar extends React.PureComponent {
const { status, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const mutingConversation = status.get('muted');
let menu = [];
@ -154,7 +155,7 @@ export default class ActionBar extends React.PureComponent {
}
if (me === status.getIn(['account', 'id'])) {
if (publicStatus) {
if (pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
@ -191,7 +192,7 @@ export default class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
);
let reblogIcon = 'retweet';
let reblogIcon = 'repeat';
//if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
// else if (status.get('visibility') === 'private') reblogIcon = 'lock';

View File

@ -15,6 +15,16 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
import classNames from 'classnames';
import PollContainer from 'flavours/glitch/containers/poll_container';
import { me } from 'flavours/glitch/util/initial_state';
const dateFormatOptions = {
month: 'numeric',
day: 'numeric',
year: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
};
export default class DetailedStatus extends ImmutablePureComponent {
@ -114,10 +124,12 @@ export default class DetailedStatus extends ImmutablePureComponent {
let media = null;
let mediaIcon = null;
let applicationLink = '';
let reblogLink = '';
let reblogIcon = 'retweet';
let reblogIcon = 'repeat';
let favouriteLink = '';
let sharekeyLinks = '';
let destructIcon = '';
let rejectIcon = '';
if (this.props.measureHeight) {
outerStyle.height = `${this.state.height}px`;
@ -168,10 +180,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
mediaIcon = 'link';
}
if (status.get('application')) {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
}
if (status.get('visibility') === 'direct') {
reblogIcon = 'envelope';
} else if (status.get('visibility') === 'private') {
@ -183,43 +191,75 @@ export default class DetailedStatus extends ImmutablePureComponent {
} else if (this.context.router) {
reblogLink = (
<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
<i className={`fa fa-${reblogIcon}`} />
<span className='detailed-status__reblogs'>
<FormattedNumber value={status.get('reblogs_count')} />
</span>
<i className={`fa fa-${reblogIcon}`} title={status.get('reblogs_count')} />
</Link>
);
} else {
reblogLink = (
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<i className={`fa fa-${reblogIcon}`} />
<span className='detailed-status__reblogs'>
<FormattedNumber value={status.get('reblogs_count')} />
</span>
<i className={`fa fa-${reblogIcon}`} title={status.get('reblogs_count')} />
</a>
);
}
if (status.get('sharekey')) {
sharekeyLinks = (
<span>
<a href={`${status.get('url')}?key=${status.get('sharekey')}`} target='_blank' className='detailed-status__link'>
<i className='fa fa-key' title='Right-click or long press to copy share link with key' />
</a>
&nbsp;·&nbsp;
<a href={`${status.get('url')}?rekey=1&toweb=1`} className='detailed-status__link'>
<i className='fa fa-user-plus' title='Generate a new share key' />
</a>
&nbsp;·&nbsp;
<a href={`${status.get('url')}?rekey=0&toweb=1`} className='detailed-status__link'>
<i className='fa fa-user-times' title='Revoke share key' />
</a>
&nbsp;·
</span>
);
} else if (status.getIn(['account', 'id']) == me) {
sharekeyLinks = (
<span>
<a href={`${status.get('url')}?rekey=1&toweb=1`} className='detailed-status__link'>
<i className='fa fa-user-plus' title='Generate a new share key' />
</a>
&nbsp;·
</span>
);
}
if (this.context.router) {
favouriteLink = (
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
<i className='fa fa-star' />
<span className='detailed-status__favorites'>
<FormattedNumber value={status.get('favourites_count')} />
</span>
<i className='fa fa-star' title={status.get('favourites_count')} />
</Link>
);
} else {
favouriteLink = (
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
<i className='fa fa-star' />
<span className='detailed-status__favorites'>
<FormattedNumber value={status.get('favourites_count')} />
</span>
<i className='fa fa-star' title={status.get('favourites_count')} />
</a>
);
}
if (status.get('delete_after')) {
destructIcon = (
<span>
<i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} /> ·
</span>
)
}
if (status.get('reject_replies')) {
rejectIcon = (
<span>
<i className='fa fa-microphone-slash' title='Rejecting replies' /> ·
</span>
)
}
return (
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', { compact })} data-status-by={status.getIn(['account', 'acct'])}>
@ -241,9 +281,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
/>
<div className='detailed-status__meta'>
{sharekeyLinks} {reblogLink} · {favouriteLink} · {destructIcon} {rejectIcon} <VisibilityIcon visibility={status.get('visibility')} />
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
</a>
</div>
</div>
</div>

View File

@ -30,8 +30,8 @@ class NotificationsIcon extends React.PureComponent {
}
export const links = [
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
<NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,

View File

@ -500,6 +500,7 @@ export default class UI extends React.Component {
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/accounts/:accountId/reblogs' component={AccountTimeline} content={children} componentParams={{ reblogs: true }} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />

View File

@ -6,26 +6,26 @@ const messages = {
'layout.current_is': 'Your current layout is:',
'layout.desktop': 'Desktop',
'layout.mobile': 'Mobile',
'navigation_bar.app_settings': 'App settings',
'getting_started.onboarding': 'Show me around',
'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.',
'navigation_bar.app_settings': 'UI options',
'getting_started.onboarding': 'Tutorial',
'onboarding.page_one.federation': '{domain} is a \'instance\' of Monsterpit. Monsterpit is a network of independent servers joining up to make one larger social network. We call these servers communities.',
'onboarding.page_one.welcome': 'Welcome to {domain}!',
'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Monsterpit}, and is compatible with any Monsterpit community or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
'settings.auto_collapse': 'Automatic collapsing',
'settings.auto_collapse_all': 'Everything',
'settings.auto_collapse_lengthy': 'Lengthy toots',
'settings.auto_collapse_media': 'Toots with media',
'settings.auto_collapse_notifications': 'Notifications',
'settings.auto_collapse_reblogs': 'Boosts',
'settings.auto_collapse_lengthy': 'Lengthy roars',
'settings.auto_collapse_media': 'Roars with media',
'settings.auto_collapse_notifications': 'Growls',
'settings.auto_collapse_reblogs': 'Repeats',
'settings.auto_collapse_replies': 'Replies',
'settings.show_action_bar': 'Show action buttons in collapsed toots',
'settings.show_action_bar': 'Show action buttons in collapsed roars',
'settings.close': 'Close',
'settings.collapsed_statuses': 'Collapsed toots',
'settings.enable_collapsed': 'Enable collapsed toots',
'settings.collapsed_statuses': 'Collapsed roars',
'settings.enable_collapsed': 'Enable collapsed roars',
'settings.general': 'General',
'settings.image_backgrounds': 'Image backgrounds',
'settings.image_backgrounds_media': 'Preview collapsed toot media',
'settings.image_backgrounds_users': 'Give collapsed toots an image background',
'settings.image_backgrounds_media': 'Preview collapsed roar media',
'settings.image_backgrounds_users': 'Give collapsed roars an image background',
'settings.media': 'Media',
'settings.media_letterbox': 'Letterbox media',
'settings.media_fullwidth': 'Full-width media previews',
@ -39,7 +39,7 @@ const messages = {
'favourite_modal.combo': 'You can press {combo} to skip this next time',
'home.column_settings.show_direct': 'Show DMs',
'home.column_settings.show_direct': 'Show whispers',
'notification.markForDeletion': 'Mark for deletion',
'notifications.clear': 'Clear all my notifications',
@ -56,11 +56,11 @@ const messages = {
'compose.attach': 'Attach...',
'advanced_options.local-only.short': 'Local-only',
'advanced_options.local-only.long': 'Do not post to other instances',
'advanced_options.local-only.tooltip': 'This post is local-only',
'advanced_options.local-only.long': 'Do not roar to other communities',
'advanced_options.local-only.tooltip': 'This roar is local-only',
'advanced_options.icon_title': 'Advanced options',
'advanced_options.threaded_mode.short': 'Threaded mode',
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
'advanced_options.threaded_mode.long': 'Automatically opens a reply on roaring',
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
};

View File

@ -45,7 +45,7 @@ import { REDRAFT } from 'flavours/glitch/actions/statuses';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from 'flavours/glitch/util/uuid';
import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
import { me, defaultContentType } from 'flavours/glitch/util/initial_state';
import { me, defaultContentType, defaultLocal, alwaysLocal } from 'flavours/glitch/util/initial_state';
import { overwrite } from 'flavours/glitch/util/js_helpers';
import { unescapeHTML } from 'flavours/glitch/util/html';
import { recoverHashtags } from 'flavours/glitch/util/hashtag';
@ -59,7 +59,7 @@ const glitchProbability = 1 - 0.0420215528;
const initialState = ImmutableMap({
mounted: false,
advanced_options: ImmutableMap({
do_not_federate: false,
do_not_federate: defaultLocal || alwaysLocal,
threaded_mode: false,
}),
sensitive: false,
@ -82,7 +82,7 @@ const initialState = ImmutableMap({
suggestion_token: null,
suggestions: ImmutableList(),
default_advanced_options: ImmutableMap({
do_not_federate: false,
do_not_federate: defaultLocal || alwaysLocal,
threaded_mode: null, // Do not reset
}),
default_privacy: 'public',
@ -177,7 +177,7 @@ function continueThread (state, status) {
map.set('in_reply_to', status.id);
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(status.content) }))
map => map.merge(new ImmutableMap({ do_not_federate: /(?:#|&num;|&#35;)(?:!|&excl;|&#33;)(?:<\/p>)?$/.test(status.content) }))
);
map.set('privacy', status.visibility);
map.set('sensitive', false);
@ -331,7 +331,7 @@ export default function compose(state = initialState, action) {
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) }))
map => map.merge(new ImmutableMap({ do_not_federate: /(?:#|&num;|&#35;)(?:!|&excl;|&#33;)(?:<\/p>)?$/.test(action.status.get('content')) }))
);
map.set('focusDate', new Date());
map.set('caretPosition', null);
@ -352,6 +352,15 @@ export default function compose(state = initialState, action) {
});
case COMPOSE_REPLY_CANCEL:
state = state.setIn(['advanced_options', 'threaded_mode'], false);
return state.withMutations(map => {
map.set('in_reply_to', null);
map.set('privacy', state.get('default_privacy'));
map.update(
'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
);
map.set('idempotencyKey', uuid());
});
case COMPOSE_RESET:
return state.withMutations(map => {
map.set('in_reply_to', null);

View File

@ -11,15 +11,15 @@ const initialState = ImmutableMap({
navbar_under : false,
swipe_to_change_columns: true,
side_arm : 'none',
side_arm_reply_mode : 'keep',
side_arm_reply_mode : 'restrict',
show_reply_count : false,
always_show_spoilers_field: false,
always_show_spoilers_field: true,
confirm_missing_media_description: false,
confirm_before_clearing_draft: true,
preselect_on_reply: true,
inline_preview_cards: true,
hicolor_privacy_icons: false,
show_content_type_choice: false,
hicolor_privacy_icons: true,
show_content_type_choice: true,
content_warnings : ImmutableMap({
auto_unfold : false,
filter : null,

View File

@ -80,8 +80,9 @@ const initialState = ImmutableMap({
const defaultColumns = fromJS([
{ id: 'COMPOSE', uuid: uuid(), params: {} },
{ id: 'HOME', uuid: uuid(), params: {} },
{ id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
{ id: 'HOME', uuid: uuid(), params: {} },
{ id: 'COMMUNITY', uuid: uuid(), params: {} },
]);
const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val);

View File

@ -41,10 +41,16 @@ export const getFilters = (state, { contextType }) => state.get('filters', Immut
const escapeRegExp = string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export const regexFromFilters = filters => {
if (filters.size === 0) {
return null;
}
export const regexFromFilters = (status, filters) => {
if (filters === undefined || filters.size === 0) { return null; }
let has_media = status.get('media_attachments').size !== 0;
filters = filters.filter(filter => {
return (!has_media && filter.get('exclude_media')) || (has_media && filter.get('media_only')) || (!filter.get('exclude_media') && !filter.get('media_only'))
});
if (filters.size === 0) { return null; }
return new RegExp(filters.map(filter => {
let expr = escapeRegExp(filter.get('phrase'));
@ -78,10 +84,10 @@ export const makeGetStatus = () => {
return null;
}
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters);
let filtered = false;
if (statusReblog) {
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(statusReblog, filters);
filtered = regex && regex.test(statusReblog.get('search_index'));
statusReblog = statusReblog.set('account', accountReblog);
statusReblog = statusReblog.set('filtered', filtered);
@ -89,6 +95,7 @@ export const makeGetStatus = () => {
statusReblog = null;
}
const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(statusBase, filters);
filtered = filtered || regex && regex.test(statusBase.get('search_index'));
return statusBase.withMutations(map => {

View File

@ -201,6 +201,7 @@
.account-role {
display: inline-block;
padding: 4px 6px;
margin-right: 2px;
cursor: default;
border-radius: 3px;
font-size: 12px;
@ -221,6 +222,30 @@
background-color: rgba(lighten($error-red, 12%), 0.1);
border-color: rgba(lighten($error-red, 12%), 0.5);
}
&.gently {
color: lighten(cyan, 25%);
background-color: rgba(lighten(cyan, 25%), 0.1);
border-color: rgba(lighten(cyan, 25%), 0.1);
}
&.kobold {
color: lighten(orange, 22%);
background-color: rgba(lighten(orange, 33%), 0.1);
border-color: rgba(lighten(orange, 33%), 0.1);
}
&.locked {
color: lighten(pink, 5%);
background-color: rgba(pink, 0.1);
border-color: rgba(pink, 0.5);
}
&.froze {
color: lighten($warning-red, 12%);
background-color: rgba(lighten($warning-red, 12%), 0.1);
border-color: rgba(lighten($warning-red, 12%), 0.5);
}
}
.account__header__fields {
@ -242,10 +267,7 @@
box-sizing: border-box;
padding: 14px;
text-align: center;
max-height: 48px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
dt {

View File

@ -0,0 +1,4 @@
@import 'variables';
@import 'arachnia/variables';
@import 'index';
@import 'arachnia/diff';

View File

@ -0,0 +1,378 @@
*:root {
// Main body
body,
body.app-body {
background: #160011 url(/system/custom-images/monsterpit-bg.png) no-repeat center center fixed !important;
background-size: cover !important;
font-size: 14px !important;
}
// Abouts: - dotted HRs
// - HR fills width of content area
// - HR adds line of blank space before/after
hr { border: 1px dotted #600; }
// Small-caps links
a[href], h1, h2, h3, h4,
.column-header,
.column-back-button {
text-decoration: none !important;
font-variant: small-caps !important;
}
.mascot-container, .floats { display: none !important; }
.mascot-container, .floats { display: none !important; }
.about-short { background: transparent !important; }
.closed-registrations-message,
.simple-form { min-height: inherit !important; }
.landing-page .heading { padding-bottom: 0 !important; }
.landing-page h1 { font-size: 32px !important; }
.landing-page p,
.landing-page li,
.features-list__row .text { color: #fff !important; }
.landing-page h1,
.landing-page h2,
.landing-page h3,
.landing-page h6 { color: #906 !important; }
.about-body h2 { font-size: 28px !important; }
.name { font-size: 24px !important; }
// Public user TL: action icons aligned with left edge of status
.activity-stream .pre-header .pre-header__icon {
position: inherit !important;
float: left;
margin-right: 0.5em !important;
left: 0 !important;
}
// Public user TL: remove intentation from action text; move down
.activity-stream .pre-header {
padding-left: 0 !important;
margin-bottom: 4px !important;
}
// User list: expand card size; one per row
.account-grid-card { width: 100% !important; }
// TL status, user card: - black semi-trans bg with rounded border
// - space between right edge and scrollbar
.status,
.detailed-status,
.detailed-status__action-bar,
.account-grid-card {
background: #302 url(/system/custom-images/status-bg.png) repeat-x top center !important;
border: 1px solid #604 !important;
border-radius: 4px;
margin: 0em 0.5em 1em 0em;
}
.status.collapsed .status__content:after {
background: linear-gradient(transparentize(#302, 1), #302) !important;
}
// TL status prefix: move origin user text closer to icon
.notification__message,
.status__prepend {
margin-left: 30px !important;
padding: 0 !important;
}
// TL status prefix: hide boost/fav action text
.notification__message span,
.status__prepend span,
.activity-stream .pre-header { font-size: 12px !important; }
// TL status: font size of user's friendly name
.notification__message span a,
.status__prepend span a,
.activity-stream .pre-header__icon,
.account__display-name strong,
.status__display-name strong,
.detailed-status__display-name strong,
.account-grid-card .username,
.name .username {
font-size: 16px !important;
font-weight: bold !important;
}
// TL status prefix: move icon closer to left edge
.notification__favourite-icon-wrapper,
.status__prepend-icon-wrapper { left: -25px !important; }
// Spoilers
.media-spoiler { background: #000 !important; }
.media-gallery.full-width { margin-left: 0; margin-right: 0 }
// UI: remove borders and solid bg colors
.ui,
.drawer,
.scrollable,
.drawer__inner,
.column-link,
.sidebar ul ul,
.column-header,
.column-header__button,
.column-back-button,
.column-header__wrapper,
.drawer__header,
.activity-stream .entry,
.accounts-grid,
.account-grid-card__header,
#mastodon-timeline,
.header-wrapper,
.about-mastodon,
.container,
.content,
.empty-column-indicator,
.learn-more-cta,
.sidebar-wrapper,
.closed-registrations-message,
.simple_form,
.information-board,
.about-short {
background: none !important;
-webkit-box-shadow: none !important;
box-shadow: none !important;
border: none !important;
}
// Column/Drawer headings: solid red bg and border; blank line after
.column-back-button,
.column-header,
.drawer__header,
.search,
.search-results,
.autosuggest-textarea__suggestions {
background: #604 !important;
margin-bottom: 1em !important;
border: 1px solid #c08 !important
}
.search-results, .autosuggest-textarea__suggestions { color: #fff !important; }
// Fix column back buttons.
.column-header__back-button {
color: #fff !important;
background: none !important;
}
.column-back-button {
top: calc(-1.5em - 42px) !important;
color: #fff !important;
}
// Search box: darken
.search__input {
background: #302 !important;
border: 1px solid #201 !important;
}
// Tootbox
.autosuggest-textarea__textarea,
.compose-form__modifiers,
.compose-form__buttons,
.spoiler-input__input {
background: #302 !important;
color: #fff !important;
}
// Reply indicator: theme like status
.reply-indicator {
background: #302 !important;
border: 1px solid #604 !important;
border-radius: 4px;
margin-bottom: 1em;
}
// TL status: move timestamp to bottom-right
.status__relative-time {
color: #906 !important;
border-bottom: none !important;
font-size: 10px !important;
position: absolute;
right: 4px;
bottom: 0;
}
// TL status: color of user address; push down post content
.account__header__username,
.accounts-grid .account-grid-card .username,
.activity-stream .status.light .display-name span,
.detailed-status__display-name,
.name .username,
.name small,
.status__display-name,
.display-name__account {
color: #a39 !important;
margin-bottom: 1em;
font-size: 12px !important;
font-weight: bold !important;
}
// TL status: color of users' friendly names; on own line
.account-grid-card .name a,
.account__display-name strong,
.detailed-status__display-name strong,
.reply-indicator__display-name,
.status__display-name strong,
.account__header__display-name,
.card__bio .name {
color: #c06 !important;
display: block;
font-weight: bold !important;
}
.status__display-name strong { display: inherit !important; }
.status__prepend span { color: white }
// TL status prefix: color of users' friendly names
.status__prepend .status__display-name,
.notification__display-name,
.status__display-name.muted,
.status__display-name.muted strong, { color: #906 !important; font-size: 14px !important; }
// Opened status: add link icon on posts
.detailed-status__datetime:before { content: "\1F517" }
// All status: message text
.reply-indicator__content,
.status__content,
.account-grid-card .note {
color: #dcd !important;
font-size: 14px !important;
line-height: inherit !important;
}
// All status: use left space; add padding to top
.status { padding-left: 10px !important; }
.status__info { padding-left: 0 !important; }
.status__content { padding-top: 10px !important; }
// All status: move icon to right side
.status__avatar {
left: inherit !important;
top: 8px !important;
right: 8px !important;
}
/// Expanded status: make header and padding match TL status
.detailed-status { padding: 8px !important; }
.detailed-status__display-name { margin: 0 !important; }
.detailed-status__display-avatar {
float: right !important;
margin: 0 !important;
}
// TL status prefix: text shouldn't clip icon
//.display-name { max-width: calc(100% - 32px) !important; }
// TL status: muted text
.muted .status__content p { color: #a9a !important; }
// TL status: links in post
.reply-indicator__content a,
.status__content a { color: #e6c !important; }
// Expanded status: action bars
.account__disclaimer, .account__action-bar { background: #160011 !important; }
// Default icon button color
.icon-button { color: #604; }
.account__header__display-name,
.account__header__username { font-variant: small-caps !important; }
.account__header__username,
.name small {
font-size: 12px !important;
font-weight: bold !important;
margin-bottom: 1em !important;
}
.account__header__content { color: #fff !important; }
// Make status Emojos bigger
.reply-indicator__content .emojione,
.status__content .emojione { width: 32px !important; height: 32px !important; padding: 2px; }
// Locked posts animation
@-webkit-keyframes blink-off {
0% { opacity: .75 }
50% { opacity: 0 }
100% { opacity: .75 }
}
@keyframes blink-off {
0% { opacity: .75 }
50% { opacity: 0 }
100% { opacity: .75 }
}
.icon-button.disabled {
opacity: .75;
-moz-transition: all 2s ease-in-out;
-webkit-transition: all 2s ease-in-out;
-o-transition: all 2s ease-in-out;
-ms-transition: all 2s ease-in-out;
transition: all 2s ease-in-out;
-moz-animation: blink-off normal 4s 5 ease-in-out;
-webkit-animation: blink-off normal 4s 5 ease-in-out;
-ms-animation: blink-off normal 4s 5 ease-in-out;
animation: blink-off normal 4s 5 ease-in-out;
}
// Active item banimation
@-webkit-keyframes blink-on {
from { transform: scale(1.5) }
50% { transform: scale(2) }
to { transform: scale(1.5) }
}
@keyframes blink-on {
from { transform: scale(1.5) }
50% { transform: scale(2) }
to { transform: scale(1.5) }
}
.column-header.active>.column-header__icon,
.icon-button.active {
color: #c08 !important;
transform: scale(1.5);
-moz-transition: all 1s ease-in-out;
-webkit-transition: all 1s ease-in-out;
-o-transition: all 1s ease-in-out;
-ms-transition: all 1s ease-in-out;
transition: all 1s ease-in-out;
-moz-animation: blink-on normal 2s 5 ease-in-out;
-webkit-animation: blink-on normal 2s 5 ease-in-out;
-ms-animation: blink-on normal 2s 5 ease-in-out;
animation: blink-on normal 2s 5 ease-in-out;
}
// Scrollbar in Chrome/Webkit browsers
::-webkit-scrollbar-thumb {
background: #906 !important;
border: 1px solid #c09 !important;
}
}

View File

@ -0,0 +1,29 @@
$classic-base-color: #160011;
$classic-primary-color: #c69;
$classic-secondary-color: #906;
$classic-highlight-color: #c08;
$base-shadow-color: $black;
$base-overlay-background: $black;
$base-border-color: $white;
$simple-background-color: $black;
$valid-value-color: $success-green;
$error-value-color: $error-red;
$ui-base-color: $classic-base-color; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%); // Lighter darkest
$ui-primary-color: $classic-primary-color; // Lighter
$ui-secondary-color: $classic-secondary-color; // Lightest
$ui-highlight-color: $classic-highlight-color;
$primary-text-color: $white;
$darker-text-color: $ui-primary-color;
$dark-text-color: $ui-base-lighter-color;
$secondary-text-color: $ui-secondary-color;
$highlight-text-color: $ui-highlight-color;
$action-button-color: $ui-base-lighter-color;
$inverted-text-color: $ui-base-color;
$lighter-text-color: $ui-base-lighter-color;
$light-text-color: $ui-primary-color;

View File

@ -0,0 +1,56 @@
/*
// Original:
// https://github.com/computerfairies/mastodon/blob/master/app/javascript/styles/mastodon/bbcode.scss
*/
.bbcode {
&__flip-horizontal {
display: inline-block;
-webkit-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
}
&__flip-vertical {
display: inline-block;
-webkit-transform: scale(1, -1);
-ms-transform: scale(1, -1);
transform: scale(1, -1);
}
@for $i from 1 through 6 {
&__size-#{$i} {
font-size: #{6 * $i}px;
& .emojione {
width: #{6 * $i}px !important;
height: #{6 * $i}px !important;
}
& .hoverplay:hover {
padding-left: #{6 * $i}px !important;
}
}
}
@for $i from 1 through 2 {
&__size-#{$i}:hover {
font-size: 12px;
}
}
&__left { display: block; text-align: left; }
&__center { display: block; text-align: center; }
&__right { display: block; text-align: right; }
&__lfloat { float: left; }
&__rfloat { float: right; }
&__spoiler-wrapper {
background: black;
color: black;
padding: 1px 2em 1px 2em;
}
&__spoiler { color: black; visibility: hidden; }
&__spoiler-wrapper:hover > &__spoiler,
&__spoiler-wrapper:active > &__spoiler
{ color: white; visibility: visible; }
}

View File

@ -179,12 +179,10 @@
}
.notification__message {
margin-left: 42px;
padding: 8px 0 0 26px;
margin-left: 25px;
cursor: default;
color: $darker-text-color;
font-size: 15px;
position: relative;
.fa {
color: $highlight-text-color;
@ -450,20 +448,140 @@
word-break: normal;
word-wrap: break-word;
p {
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
.hoverplay:hover { padding-left: 20px }
p, pre, blockquote {
margin-bottom: 20px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
}
}
h1, h2, h3, h4, h5, h6 {
margin-top: 20px;
margin-bottom: 20px;
}
h1, h2 {
font-weight: 700;
font-size: 18px;
}
h2 {
font-size: 16px;
}
h3, h4, h5, h6 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
font-style: italic;
p:last-child {
margin-bottom: 20px;
}
}
b, strong {
font-weight: 700;
}
em, i {
font-style: italic;
}
sub {
font-size: smaller;
text-align: sub;
}
sup {
vertical-align: super;
font-size: smaller;
}
ul, ol {
margin-left: 1em;
p {
margin: 0;
}
li:last-child {
margin-bottom: 20px;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
s, del {
text-decoration: line-through;
}
hr {
border-color: lighten($dark-text-color, 10%);
}
pre, code {
color: lighten($dark-text-color, 33%);
}
mark {
background-color: #ccff15;
color: black;
}
a {
color: inherit;
text-decoration: underline;
background-color: lighten($ui-base-color, 7%);
color: darken($secondary-text-color, 10%);
text-decoration: none;
padding: 2px;
border-radius: 8px;
&:hover {
text-decoration: none;
text-decoration: underline;
color: lighten($dark-text-color, 10%);
}
&.mention {
&:hover {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
.fa {
color: $dark-text-color;
}
}
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
}
@ -556,6 +674,8 @@
height: 22px;
}
.hoverplay:hover { padding-left: 22px }
h1 {
font-size: 16px;
line-height: 24px;
@ -573,6 +693,10 @@
overflow: hidden;
text-overflow: ellipsis;
}
.roles {
white-space: normal;
}
}
}

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