Compare commits

...

341 Commits

Author SHA1 Message Date
Nicolas Constant 822ef21985
Merge pull request #639 from NicolasConstant/develop
1.7.0 PR
2024-03-09 15:32:25 -05:00
Nicolas Constant a154028a53 road to 1.7.0 2024-03-09 15:10:04 -05:00
Nicolas Constant 6a8d85f40c
Merge pull request #638 from NicolasConstant/topic_enhance-filters
Topic enhance filters
2024-03-09 02:36:17 -05:00
Nicolas Constant 04153543a9 fix filtering message 2024-03-09 02:00:05 -05:00
Nicolas Constant 92ec089eab fix error spamming, fix #605 2024-03-09 01:46:26 -05:00
Nicolas Constant 12ce0a3a4a added context filtering 2024-03-09 00:40:37 -05:00
Nicolas Constant 7a6eb9c3d2 starting handling server filters 2024-03-08 03:13:58 -05:00
Nicolas Constant 63b7c6fdf1 added filter params in status interface 2024-03-08 02:12:00 -05:00
Nicolas Constant bd75317417 retrieve full handle on post edition, fix #630 2024-03-08 01:53:28 -05:00
Nicolas Constant 74eed7e8ba fix pleroma vote count, fix #398 2024-03-08 00:51:27 -05:00
Nicolas Constant ebce6282c5 better follow workflow, fix #629 2024-03-08 00:19:39 -05:00
Nicolas Constant 702e4daa44
Merge pull request #637 from NicolasConstant/topic_refine-css-zones
Topic refine css zones
2024-03-07 18:44:40 -05:00
Nicolas Constant d2221d539c fix undefined image description, fix #632 2024-03-07 18:23:26 -05:00
Nicolas Constant c4de387f86 sort list in alphabetical order, fix #633 2024-03-07 01:17:04 -05:00
Nicolas Constant c0f84ddc11 open status from image galery, fix #627 2024-03-07 00:59:00 -05:00
Nicolas Constant 1830212a91 flex migration of account navigation 2024-03-07 00:24:27 -05:00
Nicolas Constant 46adf207bb flex migration floating column + hover timeline 2024-03-06 23:13:46 -05:00
Nicolas Constant 909b190b33 fix tests 2024-03-06 19:14:30 -05:00
Nicolas Constant cfc4d5f915 GoToSocial max char handling, fix #576 2024-03-05 18:15:43 -05:00
Nicolas Constant 0f58252c61
Merge pull request #614 from NicolasConstant/develop
1.6.0 PR
2023-08-24 00:30:44 -04:00
Nicolas Constant 0d2ac6b569
road to 1.6.0 2023-08-23 23:43:50 -04:00
Nicolas Constant e62987b11a
Merge pull request #611 from NicolasConstant/topic_reorganize-accounts
Topic reorganize accounts
2023-08-20 03:35:34 -04:00
Nicolas Constant 8cee7289eb
Merge pull request #612 from NicolasConstant/topic_add-more-user-actions
Topic add more user actions
2023-08-20 03:33:32 -04:00
Nicolas Constant 0305cc6ac7
added button to unlock icons, fix #489 2023-08-20 02:33:03 -04:00
Nicolas Constant f215d027f9
added draggable aspect in service 2023-08-20 01:44:46 -04:00
Nicolas Constant 335cbf4956
added reorder state changing 2023-08-20 01:33:07 -04:00
Nicolas Constant b41c31b4ac
better draggable design 2023-08-20 01:16:41 -04:00
Nicolas Constant 41faa36087
working drag&drop 2023-08-20 00:36:52 -04:00
Nicolas Constant 024042959e
fix poll exception on creation 2023-08-19 21:33:43 -04:00
Nicolas Constant f4c87df078
added multi account faq, fix #608 2023-08-19 21:28:20 -04:00
Nicolas Constant d24441343a
always show cw text, fix #480 2023-08-19 20:39:37 -04:00
Nicolas Constant 8c9685045e
added hide boosts, block domain, fix #487 2023-08-19 20:02:37 -04:00
Nicolas Constant a0cb240446
Merge pull request #610 from NicolasConstant/topic_enhance-language-feature
Topic enhance language feature
2023-08-19 16:45:03 -04:00
Nicolas Constant 2def5725f5
option to stop animated avatar, fix #335 2023-08-12 18:41:41 -04:00
Nicolas Constant 450a0088d5
added alt label display option, fix #387 2023-08-12 17:59:34 -04:00
Nicolas Constant d7f988ecb9
added lang autodetect disable option 2023-08-12 17:16:57 -04:00
Nicolas Constant 8703df27d5
display years, fix #603 2023-08-12 17:05:25 -04:00
Nicolas Constant 10fa412173
ensure language is set to provide translation 2023-08-12 16:50:35 -04:00
Nicolas Constant 0b93ed7307
language autodetection functionnal 2023-08-12 01:33:07 -04:00
Nicolas Constant c3cd6fe79e
Merge pull request #604 from NicolasConstant/develop
1.5.0 PR
2023-08-07 20:09:31 -04:00
Nicolas Constant 14287b476c
ensure lang exists before using it 2023-08-07 19:56:46 -04:00
Nicolas Constant 2b106ba546
clean up 2023-08-07 19:48:23 -04:00
Nicolas Constant 4a2b408c1b
added some translation cleanup 2023-08-07 19:47:54 -04:00
Nicolas Constant 92a3ac6ae3
road to 1.5.0 2023-08-07 01:18:08 -04:00
Nicolas Constant ec0bed4606
wording: expand CW instead of hide, fix #459 2023-08-06 17:39:28 -04:00
Nicolas Constant 62d4140d63
close panel and unfocus on esc, fix #429 2023-08-06 17:15:00 -04:00
Nicolas Constant 4a34063dc8
focus on seach, fix #429 (partialy) 2023-08-06 17:06:36 -04:00
Nicolas Constant 9cd709f44c
better follow hastag positionning 2023-08-06 17:02:05 -04:00
Nicolas Constant 64ceb3e095
fix translate button spacing 2023-08-06 16:59:03 -04:00
Nicolas Constant cb58be5bd8
fine tunning prewrap 2023-08-06 16:50:49 -04:00
Nicolas Constant 7a8dfd0c6b
notify edited boosted status, fix #542 2023-08-06 04:41:10 -04:00
Nicolas Constant 89c5c33de2
load poll on edition, fix #587 2023-08-06 02:53:24 -04:00
Nicolas Constant 590627bc58
fix spacing, fix #597 2023-08-06 02:01:53 -04:00
Nicolas Constant 7013d9174c
enhance bookmarks support, fix #583 2023-08-06 01:17:04 -04:00
Nicolas Constant ba08c0d0b2
fine tuning dropdown menu 2023-08-05 18:59:53 -04:00
Nicolas Constant 26a01b5c30
refactoring 2023-08-05 18:40:46 -04:00
Nicolas Constant 73ac37a8f4
added translation revert 2023-08-05 18:32:23 -04:00
Nicolas Constant 38b052f06b
Merge pull request #602 from NicolasConstant/topic_lang-electron-integration
Topic lang electron integration
2023-08-05 18:13:07 -04:00
Nicolas Constant 4511363408
changed ipc channel name 2023-08-05 18:03:22 -04:00
Nicolas Constant c0f03570a0
clean up 2023-08-05 02:48:24 -04:00
Nicolas Constant 3d5c91a12b
working poc 2023-08-05 02:07:34 -04:00
Nicolas Constant 27b22338c9
Merge pull request #601 from NicolasConstant/topic_language-translation
Topic language translation
2023-08-05 00:20:39 -04:00
Nicolas Constant 191bd936aa
Merge pull request #600 from NicolasConstant/topic_language-support
Topic language support
2023-08-05 00:20:18 -04:00
Nicolas Constant 1c42f54db0
added languages 2023-08-05 00:00:00 -04:00
Nicolas Constant e8dbe214f4
sort and slice results 2023-08-04 23:45:34 -04:00
Nicolas Constant 8cd4d30ac8
notify error 2023-08-04 23:38:29 -04:00
Nicolas Constant 30f678af04
translation working 2023-08-04 23:36:21 -04:00
Nicolas Constant 16bbf9aa2f
displaying translation link 2023-08-04 22:57:06 -04:00
Nicolas Constant 74af61ad78
added warning 2023-08-04 20:23:40 -04:00
Nicolas Constant 449506092a
better language handling in settings + ui fix 2023-08-04 20:12:23 -04:00
Nicolas Constant b37a2a2f0c
fix impot 2023-08-04 03:26:24 -04:00
Nicolas Constant 32efac5aa4
language posting working 2023-08-04 03:20:48 -04:00
Nicolas Constant 91b2f4a0f0
changing lang working 2023-08-04 02:39:59 -04:00
Nicolas Constant 0d7821cd01
added selected language data 2023-08-04 01:08:00 -04:00
Nicolas Constant 18d6b8d96c
added lang change 2023-08-04 00:48:38 -04:00
Nicolas Constant 503cb6c9d4
fix language saving 2023-08-03 22:44:14 -04:00
Nicolas Constant 98e7d54c33
starting lang ui settings 2023-08-03 18:45:55 -04:00
Nicolas Constant dbb5d8e71b
added language service 2023-08-03 03:27:21 -04:00
Nicolas Constant a77b46755f
Merge pull request #599 from NicolasConstant/develop
1.4.0 PR
2023-08-02 19:15:28 -04:00
Nicolas Constant a5f9feb10b
road to 1.4.0 2023-08-02 18:58:09 -04:00
Nicolas Constant 95c4d8b249
small refactoring of the domain name display 2023-08-02 18:57:43 -04:00
Nicolas Constant 128dfd7fe5
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2023-08-02 18:49:47 -04:00
Nicolas Constant 2dc77dd39a
Merge pull request #595 from HamzaFarooqArif/589_add_items_to_header
added a new option for timelines
2023-08-02 18:50:25 -04:00
Nicolas Constant f71e175375
better wording 2023-08-02 18:48:29 -04:00
HamzaFarooqArif a1a56e49f5 reverted code that breaks spacing 2023-07-31 10:06:59 +05:00
HamzaFarooqArif 5dc98c677e added a new option for timelines 2023-07-23 00:04:20 +05:00
Nicolas Constant b00c52ff83
Merge pull request #575 from NicolasConstant/develop
1.3.0 PR
2023-04-24 18:34:37 -04:00
Nicolas Constant f46d7d433a
Merge pull request #574 from NicolasConstant/topic_fixing-things
Topic fixing things
2023-04-24 01:34:42 -04:00
Nicolas Constant 06dbdef1dc
Merge branch 'topic_fixing-things' of https://github.com/NicolasConstant/sengis into topic_fixing-things 2023-04-24 01:09:50 -04:00
Nicolas Constant 5e865ed9a4
road to 1.3.0 2023-04-24 01:09:48 -04:00
Nicolas Constant 253ea52590
Update README.md 2023-04-24 01:08:59 -04:00
Nicolas Constant 84a4b8c00a
disable notification on unsupported type 2023-04-24 01:04:44 -04:00
Nicolas Constant 982a670352
keep attachments on edition, fix #563 2023-04-24 00:17:13 -04:00
Nicolas Constant 314c736cf4
only apply block with current acc, better interact 2023-04-23 19:49:08 -04:00
Nicolas Constant 9999944d1f
added unmute / unblock, fix #341 2023-04-23 19:21:54 -04:00
Nicolas Constant 2bcac4622a
fix error message, fix #561 2023-04-23 18:40:42 -04:00
Nicolas Constant 5d6672f379
added macos cmd to send toot, fix #515 2023-04-23 18:32:42 -04:00
Nicolas Constant eac8c6120a
ensure all alt media are updated, fix #430 2023-04-23 18:27:47 -04:00
Nicolas Constant 22cad9e22d
supporting new instance v2 model 2023-04-23 16:48:07 -04:00
Nicolas Constant 232a86566c
support v2 instance, fix #504 2023-04-23 16:20:00 -04:00
Nicolas Constant 2cb443dd4d
fix streaming url retrieval, fix #565 2023-04-23 16:09:44 -04:00
Nicolas Constant cb342ce9b5
Merge pull request #571 from NicolasConstant/topic_updated-docs
updated electron build
2023-04-23 15:45:28 -04:00
Nicolas Constant 8c9fe07109
Merge pull request #573 from NicolasConstant/topic_fixing-things
Topic fixing things
2023-04-23 15:32:32 -04:00
Nicolas Constant 00134a7407
electron removal 2023-04-23 15:23:43 -04:00
Nicolas Constant db6b37eef3
add auth on emoji fetch, fix #391 2023-04-23 15:09:34 -04:00
Nicolas Constant e14852e087
Merge branch 'topic_fixing-things' of https://github.com/NicolasConstant/sengis into topic_fixing-things 2023-04-23 15:08:39 -04:00
Nicolas Constant 6001a26f02
fix dependancies 2023-04-23 15:08:35 -04:00
Nicolas Constant 48677e8e6c
Delete build.yml 2023-04-23 02:53:12 -04:00
Nicolas Constant 1ca603f211
removed electron 2023-04-23 02:52:33 -04:00
Nicolas Constant d60bf804b8
Delete build.yml 2023-04-23 01:59:40 -04:00
Nicolas Constant 8bd71afc55
updated electron build 2023-04-23 01:55:02 -04:00
Nicolas Constant ed8c935285
Update build.yml 2023-04-13 00:52:02 -04:00
Nicolas Constant b1cd975422
updated electron 2023-04-12 23:56:27 -04:00
Nicolas Constant c5e3f4abac
added new CICD for Electron builds 2023-04-12 23:38:46 -04:00
Nicolas Constant 4599d64c60
Merge pull request #517 from NicolasConstant/develop
1.2.0 PR
2022-12-10 22:53:48 -05:00
Nicolas Constant 522c1c0133
road to 1.2.0 2022-12-10 22:34:47 -05:00
Nicolas Constant b6ea1d8d43
fix autocomplete error 2022-12-10 22:26:06 -05:00
Nicolas Constant 55a855d046
align post button text 2022-12-10 22:19:12 -05:00
Nicolas Constant 410007dc25
added edit text 2022-12-10 22:11:25 -05:00
Nicolas Constant 54d4b300f4
Merge pull request #516 from NicolasConstant/topic_edit-status_enhancements
Topic edit status enhancements
2022-12-10 19:57:48 -05:00
Nicolas Constant f4ba3a168f
removing editstatus panel 2022-12-10 19:41:43 -05:00
Nicolas Constant f2e1478cfa
refactoring panels for edition 2022-12-10 19:40:17 -05:00
Nicolas Constant ce71965b5c
refactoring 2022-12-10 19:19:02 -05:00
Nicolas Constant 65c147bc6f
clean up 2022-12-10 18:55:20 -05:00
Nicolas Constant 57f863e2a1
added edition notification logic 2022-12-10 18:31:54 -05:00
Nicolas Constant 0ce8be99bd
Merge branch 'develop' into topic_edit-status 2022-12-08 00:11:14 -05:00
Nicolas Constant f5de97993b
Merge pull request #478 from rpetti/add-edit-posts
Support Mastodon 4.0.0's Edit Feature
2022-12-07 23:31:00 -05:00
Nicolas Constant 0777c23124
Merge pull request #513 from NicolasConstant/fix_item-count-slow-mode
Fix item count slow mode
2022-12-07 23:04:22 -05:00
Nicolas Constant 70c9e2564b
Update appveyor.yml 2022-12-07 22:47:27 -05:00
Nicolas Constant 54772d8487
fix build 2022-12-07 22:30:04 -05:00
Nicolas Constant 30c81ae143
Merge pull request #499 from rpetti/fix-slow-mode-new-item-count
fix new item count in slow mode
2022-12-07 22:21:29 -05:00
Nicolas Constant 9cc2324fd2
remove duplicate tag 2022-12-02 00:01:30 -05:00
Nicolas Constant c912f12db5
Merge pull request #472 from rpetti/add-tag-following
hashtag following support for mastodon >= 4.0
2022-12-02 00:00:45 -05:00
Rob Petti 513bb1e684 fix new item count in slow mode 2022-11-26 13:48:33 -07:00
Rob Petti ec233754dd add post edit functionality 2022-11-19 10:16:31 -07:00
Rob Petti 39187c82fb initial implementation of tag following 2022-11-19 08:58:33 -07:00
Nicolas Constant 78f0f3ab5f
Merge pull request #474 from NicolasConstant/develop
1.1.6 PR
2022-11-18 20:34:03 +01:00
Nicolas Constant 39abd6a175
road to 1.1.6 2022-11-18 19:53:36 +01:00
Nicolas Constant 644b0d0b86
Merge pull request #471 from rpetti/fix-bookmarks
fixing the version check on the bookmarks feature
2022-11-18 19:51:44 +01:00
Rob Petti 83f52391ae fixing the version check on the bookmarks feature 2022-11-17 16:08:13 -07:00
Nicolas Constant 33a61f7347
Merge pull request #404 from NicolasConstant/develop
1.1.5 PR
2022-01-21 22:53:49 -05:00
Nicolas Constant 0409431105
removing travis 2022-01-21 21:40:16 -05:00
Nicolas Constant 42fb269c24
road to 1.1.5 2022-01-19 23:30:38 -05:00
Nicolas Constant c3a5306e56
Merge pull request #403 from NicolasConstant/fix_soapbox-mess
Fix missing mentions
2022-01-19 23:29:54 -05:00
Nicolas Constant 76b911351c
Merge pull request #402 from NicolasConstant/fix_cicd
Fix CICD
2022-01-19 23:05:39 -05:00
Nicolas Constant 7cb0887749
disable cache 2022-01-19 22:39:44 -05:00
Nicolas Constant 5c52c9c4f2
bump nodejs version 2022-01-19 22:37:58 -05:00
Nicolas Constant 59c3b19271
fix npm version 2022-01-19 22:32:50 -05:00
Nicolas Constant 2f84471a3e
add missing mentions, fix #399 2022-01-19 22:08:19 -05:00
Nicolas Constant 640028ca08
Merge pull request #388 from NicolasConstant/develop
1.1.4 PR
2021-07-15 17:53:55 -04:00
Nicolas Constant 3f01c70bc9
changed electron-builder version 2021-07-15 00:49:59 -04:00
Nicolas Constant 70bef7b98e
changed eletron-builder version 2021-07-15 00:43:26 -04:00
Nicolas Constant 0956b623ce
changed electron-builder version 2021-07-15 00:31:33 -04:00
Nicolas Constant 6554a359b5
CICD 2021-07-15 00:09:27 -04:00
Nicolas Constant 1ebbece7ab
CICD 2021-07-15 00:05:28 -04:00
Nicolas Constant a85e24b77f
road to 1.1.4 2021-07-14 21:04:58 -04:00
Nicolas Constant c2812fae43
fix mention bug 2021-07-14 21:00:52 -04:00
Nicolas Constant 9426bc9e38
Merge pull request #368 from NicolasConstant/develop
1.1.3 PR
2021-03-21 14:15:06 -04:00
Nicolas Constant 06d142c4a5
fix builds 2021-03-21 13:29:59 -04:00
Nicolas Constant eb74e34cb0
force CI 2021-03-20 16:53:55 -04:00
Nicolas Constant 50dc938295
Merge pull request #367 from NicolasConstant/develop
1.1.2 PR
2021-03-20 14:54:49 -04:00
Nicolas Constant f1596bf04f
road to 1.1.2 2021-03-20 14:10:08 -04:00
Nicolas Constant 67e69c64a4
fine tunning polls 2021-03-20 14:09:36 -04:00
Nicolas Constant ba5fead320
Merge pull request #365 from NicolasConstant/develop
1.1.1 PR
2021-03-14 20:59:32 -04:00
Nicolas Constant 9bba8a3352
road to 1.1.1 2021-03-12 19:25:27 -05:00
Nicolas Constant 14a9aade0b
Merge pull request #364 from NicolasConstant/topic_fix-fav-issues
Topic fix fav issues
2021-03-12 19:22:10 -05:00
Nicolas Constant 93847df4d8
fix undoing action not updating global state 2021-03-12 19:05:10 -05:00
Nicolas Constant aa705d7c5b
chaining calls 2021-03-12 18:59:15 -05:00
Nicolas Constant 21ad2cffb6
fix app reload on double-clic on fav/boost/etc 2021-03-12 18:44:03 -05:00
Nicolas Constant f8cea22693
Merge pull request #363 from NicolasConstant/topic_fix-poll-counts
fix polls count
2021-03-11 23:25:45 -05:00
Nicolas Constant 03bcc95d65
fix polls count 2021-03-11 23:09:12 -05:00
Nicolas Constant 8bbc58d9c8
Merge pull request #359 from NicolasConstant/develop
1.1.0 PR
2021-03-08 00:45:19 +01:00
Nicolas Constant 28065912b2
added link to BSL documentation 2021-03-06 19:38:02 -05:00
Nicolas Constant cd96324442
added bridge tutorial images 2021-03-07 01:24:57 +01:00
Nicolas Constant 30cb395bda
road to 1.1.0 2021-03-06 19:00:16 -05:00
Nicolas Constant 8d13822000
Merge pull request #358 from NicolasConstant/topic_birdsitelive-support
Topic birdsitelive support
2021-03-07 00:59:33 +01:00
Nicolas Constant d82da3d180
integration of twitter bridge in search workflow 2021-02-28 02:38:42 -05:00
Nicolas Constant 7653398642
added bridge instance settings 2021-02-28 01:50:06 -05:00
Nicolas Constant fb4c99870e
wirering autofollow in list editor 2021-02-28 01:29:09 -05:00
Nicolas Constant 47a8cdc096
added autofollow settings for pleroma 2021-02-28 01:19:41 -05:00
Nicolas Constant 3724b0b4c2
fix Hometown links 2021-02-28 00:48:52 -05:00
Nicolas Constant 030ce2e568
fix Hometown hashtag, fix #357 2021-02-28 00:29:12 -05:00
Nicolas Constant 1d9e3c5130
fix lock 2021-02-28 00:27:36 -05:00
Nicolas Constant 5eef9506fe
Create FUNDING.yml 2021-02-27 16:31:23 -05:00
Nicolas Constant f152a3dc6f
Merge pull request #349 from NicolasConstant/develop
1.0.3 PR
2021-01-21 04:18:01 +01:00
Nicolas Constant d30f5a8261
road to 1.0.3 2021-01-20 22:00:59 -05:00
Nicolas Constant 4babd219b4
Encode html in CW, fix #348 2021-01-20 21:59:14 -05:00
Nicolas Constant 9ae1711093
Merge pull request #340 from NicolasConstant/develop
1.0.2 PR
2021-01-06 00:36:52 +01:00
Nicolas Constant 1b7853ec4d
road to 1.0.2 2021-01-05 16:02:38 -05:00
Nicolas Constant 6144d12740
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2021-01-05 16:01:09 -05:00
Nicolas Constant 6696ca4274
fix images size in status html, fix #339 2021-01-05 16:00:35 -05:00
Nicolas Constant 70032f55f1
only deploy on tag 2021-01-04 20:18:16 -05:00
Nicolas Constant 63175d1e60
Merge pull request #338 from NicolasConstant/develop
1.0.1 PR
2021-01-05 00:18:38 +01:00
Nicolas Constant 269b8b87cd
road to 1.0.1 2021-01-04 03:24:35 -05:00
Nicolas Constant b2a198c6d9
fix add account to list in pleroma 2021-01-04 02:13:02 -05:00
Nicolas Constant af026a444d
fix list creation in pleroma 2021-01-04 02:00:00 -05:00
Nicolas Constant 31527d3914
Merge pull request #337 from NicolasConstant/develop
1.0.0 PR
2020-12-28 00:28:36 +01:00
Nicolas Constant d74b030688
fix poll vote in pleroma 2020-12-22 19:55:12 -05:00
Nicolas Constant 438867e49a
clean up 2020-12-22 19:54:35 -05:00
Nicolas Constant 82e86039b4
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2020-12-21 23:13:11 -05:00
Nicolas Constant 8b849a6650
added move notification support 2020-12-21 23:12:47 -05:00
Nicolas Constant 8c76056747
fix typo 2020-12-21 21:14:32 -05:00
Nicolas Constant 4083b1017a
updated PR guidelines 2020-12-17 22:42:22 -05:00
Nicolas Constant 5b7c2de8ba
updated state of development 2020-12-17 22:36:46 -05:00
Nicolas Constant 3c4fc074ef
road to 1.0.0 2020-12-17 20:08:19 -05:00
Nicolas Constant e772613193
hide button when hashtag is added 2020-12-17 20:07:37 -05:00
Nicolas Constant 8566966463
added middle click support on searched accounts 2020-12-17 19:35:26 -05:00
Nicolas Constant 5f4e822b64
fix move notification breaking things 2020-12-17 19:08:07 -05:00
Nicolas Constant 7d42737c27
Merge pull request #328 from NicolasConstant/develop
0.33.0 PR
2020-10-06 01:39:01 +02:00
Nicolas Constant e5ce5fb14e
road to 0.33.0 2020-10-05 00:29:43 -04:00
Nicolas Constant 50758f1170
Merge pull request #326 from NicolasConstant/topic_follow-requests
Topic follow requests
2020-10-05 02:31:10 +02:00
Nicolas Constant d720dc06a9
Merge pull request #327 from NicolasConstant/topic_fix-icon
Topic fix icon
2020-10-05 02:30:12 +02:00
Nicolas Constant 7841f72890
added lock icon on account's profile 2020-10-03 15:52:55 -04:00
Nicolas Constant a303f16afe
typo 2020-10-03 12:44:21 -04:00
Nicolas Constant 3be94a842d
working follow_request logic 2020-10-03 00:40:43 -04:00
Nicolas Constant d67ef4aaf2
creation of follow-request UI 2020-10-02 23:58:43 -04:00
Nicolas Constant 0c361d57fc
try other icon fix 2020-10-02 20:59:36 -04:00
Nicolas Constant e46c878e36
Merge branch 'topic_fix-icon' of https://github.com/NicolasConstant/sengis into topic_fix-icon 2020-10-01 23:04:29 -04:00
Nicolas Constant 49c776a67c
try fix for KDE 2020-10-01 23:04:18 -04:00
Nicolas Constant 24c188aa80
road to 0.32.1 2020-10-01 22:33:29 -04:00
Nicolas Constant 180f218eb0
trigger CI/CD 2020-10-01 22:11:06 -04:00
Nicolas Constant c2f9c17189
refactoring 2020-10-01 22:01:09 -04:00
Nicolas Constant 45d735835b
added multi os icon specs 2020-10-01 21:58:35 -04:00
Nicolas Constant 0d1a2e59d4
added icon on main windows 2020-10-01 21:42:30 -04:00
Nicolas Constant c950744a48
Merge pull request #323 from NicolasConstant/develop
0.32.0 PR
2020-09-23 02:10:59 +02:00
Nicolas Constant 23abf0e0b7
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2020-09-21 20:19:59 -04:00
Nicolas Constant 3dbc5c57e1
changed wording of #315 2020-09-21 20:19:53 -04:00
Nicolas Constant f13e30ebaf
fine tuning tutorial's animation 2020-09-21 20:10:49 -04:00
Nicolas Constant 45620de391
Merge pull request #320 from Miosame/patch-1
Fix typos in readme
2020-09-19 23:15:11 +02:00
Nicolas Constant 711e351543
Merge pull request #321 from Miosame/patch-2
Fix typos in contributing.md
2020-09-19 23:14:33 +02:00
Miosame 91f75f2b0f
Fix typos in contributing.md
"explicitely" => "explicitly"
"opened" => "open"
2020-09-18 06:16:19 +02:00
Miosame 0afa7a0998
Fix typos
"clic" => "click"
"leaves" => "leave"
"supporting" => "supports"
2020-09-18 06:15:51 +02:00
Nicolas Constant 912d9e31b5
better misskey parsing in pleroma 2020-09-17 22:16:53 -04:00
Nicolas Constant a0952de788
bump electron 10 2020-09-17 21:40:54 -04:00
Nicolas Constant 628b9c6733
disable tests 2020-09-17 21:40:34 -04:00
Nicolas Constant bcc4549b9a
added docker documentation 2020-09-16 20:18:22 -04:00
Nicolas Constant 54bac5e0ee
Merge pull request #319 from NicolasConstant/topic_privacy-statement
added privacy statement
2020-09-14 01:33:58 +02:00
Nicolas Constant e223c1d032
road to 0.32.0 2020-09-13 18:48:05 -04:00
Nicolas Constant 0d851560b6
added privacy statement, fix #315 2020-09-13 18:45:48 -04:00
Nicolas Constant b09c2a0b81
Merge pull request #318 from NicolasConstant/topic_bump-electron
Topic bump electron
2020-09-13 23:46:30 +02:00
Nicolas Constant 386058eceb
Merge branch 'develop' into topic_bump-electron 2020-09-13 23:28:34 +02:00
Nicolas Constant 7b94b950d2
Merge pull request #317 from NicolasConstant/topic_enhance-tutorial
Topic enhance tutorial
2020-09-13 23:27:51 +02:00
Nicolas Constant 496b2b7dd2
fix tutorial navigation 2020-09-13 15:17:55 -04:00
Nicolas Constant e600f096cd
typo fix 2020-09-13 15:10:35 -04:00
Nicolas Constant b4eb092181
added option to reopen tutorial 2020-09-12 14:17:54 -04:00
Nicolas Constant f5f1c2e8f8
activating saving tutorial state 2020-09-12 14:12:59 -04:00
Nicolas Constant d46fa0ffca
better tutorial navigation 2020-09-12 14:07:30 -04:00
Nicolas Constant 63c2385644
added following functionality 2020-09-12 14:01:35 -04:00
Nicolas Constant b02979430c
added thankyou tutorial 2020-09-12 13:47:07 -04:00
Nicolas Constant 39af84785f
added label tutorial 2020-09-12 13:03:26 -04:00
Nicolas Constant 5992ac7001
added video illustrations 2020-09-12 01:09:29 -04:00
Nicolas Constant 5ae8d668df
better tutorial canvas 2020-09-11 23:13:00 -04:00
Nicolas Constant a37b814c16
fix merge 2020-09-11 22:36:36 -04:00
Nicolas Constant 60e99a1c30
Merge pull request #316 from NicolasConstant/develop
get last modifications
2020-09-12 04:35:58 +02:00
Nicolas Constant a8f940eea7
version updated 2020-09-11 22:00:36 -04:00
Nicolas Constant 9adb9c5c44
Merge branch 'topic_enhance-tutorial' into develop 2020-09-12 02:22:41 +02:00
Nicolas Constant 8f03d7f19e
disable spellchecker 2020-09-04 22:19:49 -04:00
Nicolas Constant 4e453903f2
bump electron 2020-09-04 22:12:50 -04:00
Nicolas Constant d8398a4af6
Merge pull request #312 from NicolasConstant/develop
0.31.1 PR
2020-09-03 21:44:10 -04:00
Nicolas Constant efbc3d3cdc
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2020-09-02 22:20:42 -04:00
Nicolas Constant ac3acfb4fa
tweak code display 2020-09-02 22:19:47 -04:00
Nicolas Constant 4e9730a4ae
added docker info 2020-09-02 19:20:31 -04:00
Nicolas Constant 16a92bcc56
road to 0.31.1 2020-09-02 19:09:23 -04:00
Nicolas Constant 2b0da954a1
fix multipost count 2020-09-02 18:55:07 -04:00
Nicolas Constant 0e23b64b63
fix first status count 2020-09-02 18:43:04 -04:00
Nicolas Constant 8f719c515b
fix docker image 2020-09-02 00:58:19 -04:00
Nicolas Constant 4df59f0edb
Merge pull request #310 from NicolasConstant/develop
0.31.0 PR
2020-08-29 22:54:02 +02:00
Nicolas Constant 7a0de67f5d
added middle click support on mentions 2020-08-29 14:46:38 -04:00
Nicolas Constant 65068f5dc0
added middle click support on hashtags 2020-08-29 14:30:13 -04:00
Nicolas Constant 54ed9a8c4a
fix links for docker 2020-08-29 12:55:22 -04:00
Nicolas Constant 73f6030a87
fix screen gif url for docker page 2020-08-28 23:57:08 -04:00
Nicolas Constant 713fa918be
road to 0.31.0 2020-08-28 23:54:38 -04:00
Nicolas Constant a82afc6bd1
fix favorites end condition retrieval 2020-08-28 23:08:31 -04:00
Nicolas Constant fbe5a53f60
fix bookmarks end condition retrieval 2020-08-28 23:00:53 -04:00
Nicolas Constant 505f0b025a
fix Zap mention parsing 2020-08-28 22:42:00 -04:00
Nicolas Constant 534a4b11e3
fix misskey mention parsing 2020-08-28 22:12:30 -04:00
Nicolas Constant 69c6fbc145
fix tests 2020-08-28 21:51:48 -04:00
Nicolas Constant 1f93817a6f
fix noopener on event binding 2020-08-28 21:39:55 -04:00
Nicolas Constant 58c1f04609
more resilient status search 2020-08-28 21:35:50 -04:00
Nicolas Constant f3f63f569a
block link click propagation 2020-08-28 21:18:41 -04:00
Nicolas Constant a809274756
fix url count, fix #308 2020-08-28 21:08:23 -04:00
Nicolas Constant 8710b0267e
better links, fix #309 2020-08-28 20:54:09 -04:00
Nicolas Constant 95454e29a0
make poll retrieval error silent 2020-07-12 18:36:18 -04:00
Nicolas Constant 1ae9cc282f
Merge pull request #304 from NicolasConstant/develop
0.30.1 PR
2020-07-01 02:56:31 +02:00
Nicolas Constant 031b1d5631
road to 0.30.1 2020-06-30 20:19:19 -04:00
Nicolas Constant 5ddf555172
fix #303 2020-06-30 19:58:38 -04:00
Nicolas Constant ece9182e99
Merge pull request #302 from NicolasConstant/develop
0.30.0 PR
2020-06-25 03:18:26 +02:00
Nicolas Constant 9cc21a4b64
road to 0.30.0 2020-06-24 20:26:32 -04:00
Nicolas Constant e3a7239522
fix max reached condition 2020-06-24 20:25:37 -04:00
Nicolas Constant 08dd0025c9
fix mention loading 2020-06-18 02:25:32 -04:00
Nicolas Constant b0234435d4
Merge pull request #301 from NicolasConstant/topic_followers-follows
Topic followers follows
2020-06-17 05:30:13 +02:00
Nicolas Constant bce2cd0527
added empty followers message 2020-06-16 23:09:49 -04:00
Nicolas Constant 5230b3a115
fix search display 2020-06-15 01:12:39 -04:00
Nicolas Constant d5b2a3e47d
fix account wraping and overflow 2020-06-15 01:07:30 -04:00
Nicolas Constant 9ad3ef81b4
fix emoji in account display name 2020-06-15 00:58:13 -04:00
Nicolas Constant e821c8a8dc
fix follows refresh 2020-06-15 00:56:39 -04:00
Nicolas Constant 1de96741ad
fix following term 2020-06-15 00:45:30 -04:00
Nicolas Constant 9a8f24462c
fix follower pagination 2020-06-15 00:44:00 -04:00
Nicolas Constant 34dcc3050a
added follower scrolling 2020-06-15 00:32:47 -04:00
Nicolas Constant db5ee22615
displaying accounts in follows 2020-06-14 21:11:45 -04:00
Nicolas Constant 382cae866f
created account component 2020-06-14 20:53:20 -04:00
Nicolas Constant fa0ae59e78
fix background transition 2020-06-14 20:41:45 -04:00
Nicolas Constant f6466a5c8f
added followers retrieval 2020-06-14 20:39:51 -04:00
Nicolas Constant ff030e4669
created user-follow comp and wirering to it 2020-06-14 18:12:54 -04:00
Nicolas Constant 675dd0a3a2
Merge pull request #300 from NicolasConstant/topic_fix-spamming
Topic fix spamming
2020-06-14 10:22:05 +02:00
Nicolas Constant 82a4452a41
prevent spamming, fix #293 2020-06-14 03:48:30 -04:00
Nicolas Constant c30ba1483a
browsing refactoring 2020-06-14 03:07:44 -04:00
Nicolas Constant f073e4f224
keep privacy on delete&redraft 2020-06-08 19:29:16 -04:00
Nicolas Constant b423ca6b31
better handling of errors message 2020-06-08 19:10:24 -04:00
Nicolas Constant fef906da72
fix remote status mention parsing 2020-06-06 23:12:06 -04:00
Nicolas Constant 134d2c47f5
Merge pull request #297 from NicolasConstant/develop
0.29.1 PR
2020-06-04 05:11:05 +02:00
Nicolas Constant c51ea38c96
road to 0.29.1 2020-06-03 20:24:55 -04:00
Nicolas Constant 734d2ae161
clean up 2020-06-03 20:15:26 -04:00
Nicolas Constant 5674127e28
fix caret positionning after autocompletion 2020-06-03 19:50:12 -04:00
Nicolas Constant c8c17ca44e
better autocomplete replacement 2020-06-03 19:16:16 -04:00
Nicolas Constant b0b3c4ec21
added tests on autosuggestion replacement 2020-06-03 18:23:13 -04:00
Nicolas Constant fa0d89276a
remove unecessary import 2020-05-29 03:40:56 -04:00
Nicolas Constant 9de28fad86
Merge pull request #295 from NicolasConstant/develop
Develop PR
2020-05-29 03:40:04 -04:00
Nicolas Constant 185bb64a42
added infinite scroll lock 2020-05-29 03:38:38 -04:00
Nicolas Constant 391c515b30
TimelineBase migration 2020-05-29 02:59:12 -04:00
Nicolas Constant 305833ddc2
fix bookmarks and favs background, fix #294 2020-05-29 00:42:08 -04:00
Nicolas Constant ba7872b902
fix errors on profile refresh 2020-05-29 00:32:55 -04:00
Nicolas Constant 0ee30eba76
D&R stop messing with status state, fix #291 2020-05-28 23:56:42 -04:00
Nicolas Constant 8000f51aaa
sanitizen status's links, fix #290 2020-05-28 18:34:51 -04:00
Nicolas Constant 37325cb188
Merge pull request #288 from NicolasConstant/develop
0.29.0 PR
2020-05-24 17:42:40 -04:00
Nicolas Constant 164bf22484
road to 0.29.0 2020-05-24 17:14:33 -04:00
Nicolas Constant 5f7f77a60e
fix scaling shortcuts 2020-05-24 17:13:00 -04:00
Nicolas Constant b0c16b3fa8
Merge branch 'develop' of https://github.com/NicolasConstant/sengis into develop 2020-05-24 16:40:09 -04:00
Nicolas Constant 879bfb7d23
Merge pull request #286 from yannicka/patch-1
Add the possibility to zoom the application
2020-05-24 16:39:51 -04:00
Nicolas Constant 6088be077f
added escape hotkey, fix #287 2020-05-24 16:36:10 -04:00
Yannick A a0b43be0a7
Add the possibility to zoom the application
If the text is too small, it can be handy to be able to zoom in and out.
2020-05-23 23:30:40 +02:00
Nicolas Constant 73c264e9e7
clean up 2020-05-22 02:34:08 -04:00
Nicolas Constant ab30bb1a06
refactoring onScroll 2020-05-22 02:31:14 -04:00
Nicolas Constant 1e9f0d4137
start stream refactoring 2020-05-22 02:17:26 -04:00
Nicolas Constant 93a0d17fc0
added pipe tests 2020-05-21 01:07:23 -04:00
Nicolas Constant f4ff3d0e94
ensure https on remote media 2020-05-21 01:02:34 -04:00
Nicolas Constant 0635397087
added label tutorial 2020-05-05 18:50:16 -04:00
Nicolas Constant 94fe3eff31
first draft of notification tutorial 2020-05-02 00:24:10 -04:00
Nicolas Constant 3c93dcb709
added first tutorial content 2020-05-02 00:00:31 -04:00
Nicolas Constant 8139f1a601
tutorial navigation functionnal 2020-05-01 23:48:40 -04:00
Nicolas Constant f7187353bb
created canvas to show enhanced tutorial 2020-05-01 23:08:59 -04:00
156 changed files with 6544 additions and 3263 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
patreon: nicolasconstant

View File

@ -1,47 +0,0 @@
sudo: required
dist: trusty
language: c
matrix:
include:
- os: osx
- os: linux
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
node_js:
- 10.9.0
cache:
directories:
- node_modules
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
- icnsutils
- graphicsmagick
- libgnome-keyring-dev
- xz-utils
- xorriso
- xvfb
install:
- nvm install 10.9.0
- npm install electron-builder@next
- npm install
- npm rebuild node-sass
- export DISPLAY=':99.0'
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start &
- sleep 3
script:
- npm run travis

View File

@ -20,5 +20,5 @@ For example:
## Pull Requests
Pull Requests are maybe a bit early right now, since the project and code can change a lot, so it's not really adviced to open PR today.
I will notify explicitely when I'll be more opened to external contributions.
Please open first an [issue](https://github.com/NicolasConstant/sengi/issues/new) before working on a new functionality you would like to submit to this repository.

28
DOCKER.md Normal file
View File

@ -0,0 +1,28 @@
# Sengi's Docker documentation
Here is some more detailed informations for Sengi's Docker users.
## Deploy Sengi's
Execute:
```
docker run -d -p 80:80 nicolasconstant/sengi
```
Sengi will then be available on port 80
## Landing page
Sengi's docker contains a landing page so that you can open a pop-up easily.<br />
It's available in ```https://your-host/start/index.html```
## Personalize the Privacy Statement
You can personalize the privacy statement by linking it as follow:
```
docker run -d -p 80:80 -v /Path/privacy.html:/app/assets/docs/privacy.html nicolasconstant/sengi
```

View File

@ -12,8 +12,8 @@ FROM alpine:latest
RUN apk add --update --no-cache lighttpd
ADD lighttpd.conf /etc/lighttpd/lighttpd.conf
COPY --from=build /build/dist /app/sengi
COPY --from=build /build/assets/docker_init /app
COPY --from=build /build/dist /app
COPY --from=build /build/assets/docker_init /app/start
EXPOSE 80

View File

@ -7,50 +7,66 @@ Sengi is a **Mastodon** and **Pleroma** desktop focused client. It takes inspira
It is strongly focused on the following points:
* Heavily oriented on multi-accounts usage
* Desktop based interactions (right clic, left clic, etc)
* One column at a time display (leaves it on the side of your screen, and keep an eye on it while doing your other stuff)
* Desktop based interactions (right click, left click, etc)
* One column at a time display (leave it on the side of your screen, and keep an eye on it while doing your other stuff)
It is released as a **browser webapp** and also packaged as an **cross-platform desktop application** (Mac, Windows, and Linux).
The Electron code isn't hosted here anymore, and you'll find it [here](https://github.com/NicolasConstant/sengi-electron).
## Official project page
[Discover Sengi](https://nicolasconstant.github.io/sengi/)
## State of development
Sengi already supporting all the basics functionalities, but many minors enhancements are still needed before a 1.0.0 release.
The first major stable release has been published (1.0.0), the project is open to external contributions.
## Screens
![/docs/images/presentation_small.gif](/docs/images/presentation_small.gif)
![https://raw.githubusercontent.com/NicolasConstant/sengi/master/docs/images/presentation_small.gif](https://raw.githubusercontent.com/NicolasConstant/sengi/master/docs/images/presentation_small.gif)
## Docker
A docker image is available for auto-hosting your own Sengi webapp!
```
docker run -d -p 80:80 nicolasconstant/sengi
```
Find more informations [here](https://github.com/NicolasConstant/sengi/blob/master/DOCKER.md).
The docker image also provide a landing page so that you can open a pop-up really easily. <br />
It's available in ```https://your-host/start/index.html```
## Contact
* [Official Sengi Account](https://mastodon.social/@sengi_app)
## Contribute
Please see the [contributing guidelines](https://github.com/NicolasConstant/sengi/blob/master/CONTRIBUTING.md)
## License
This project is licensed under the AGPLv3 License - see [LICENSE](https://github.com/NicolasConstant/sengi/blob/master/LICENSE) for details
## Credits
See [credits](https://github.com/NicolasConstant/sengi/blob/master/CREDITS.md)
## Dependencies
* [Angular 7](https://github.com/angular/angular)
* [NGXS](https://github.com/ngxs/store)
* [SASS](https://github.com/sass/dart-sass)
* [Electron 10](https://github.com/electron/electron)
## What's a sengi?!
It's a little [elephant shrew](https://en.wikipedia.org/wiki/Elephant_shrew) from Africa:
![Rhynchocyon petersi](https://upload.wikimedia.org/wikipedia/commons/thumb/8/81/Rhynchocyon_petersi_from_side.jpg/400px-Rhynchocyon_petersi_from_side.jpg)
## Contribute
Please see the [contributing guidelines](CONTRIBUTING.md)
## License
This project is licensed under the AGPLv3 License - see [LICENSE](LICENSE) for details
## Credits
See [credits](CREDITS.md)
## Dependencies
* [Angular 7](https://github.com/angular/angular)
* [NGXS](https://github.com/ngxs/store)
* [SASS](https://github.com/sass/dart-sass)
* [Electron 4](https://github.com/electron/electron)

View File

@ -1,15 +1,15 @@
os: unstable
cache:
- node_modules
#- node_modules
environment:
GH_TOKEN:
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
secure: eXSiJiDFgLi4vixO5GS93lgrqZ+BzQNy7PKPCQCErHjCQD9mWiEtVQQnhvmUq1FPLUc3fNLmOFQu2nIWA9bnkHg5Yw9WiG2m7QSCPRB+xCnvSY6JbLqpzURZp5x5OLj6
matrix:
- nodejs_version: 10.9.0
install:
- ps: Install-Product node $env:nodejs_version
- set CI=true
- npm install -g npm@latest
- npm install -g npm@6.9.0
- set PATH=%APPDATA%\npm;%PATH%
- npm install
matrix:
@ -45,3 +45,4 @@ deploy:
application: dist.zip
on:
branch: master
# APPVEYOR_REPO_TAG: true

View File

@ -15,11 +15,11 @@
<div class="launcher-wrapper">
<div class="launcher">
<a href="#" class="button" title="launch sengi in popup"
onClick="window.open('/sengi/'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;">
onClick="window.open('/../'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;">
<span class="download-button__web--label">Launch Sengi Popup</span>
</a><br />
<a href="/sengi/" class="button" title="launch sengi">
<a href="/../" class="button" title="launch sengi">
<span class="download-button__web--label">Open Sengi</span>
</a><br />
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -34,7 +34,7 @@
<div class="header__download-box--buttons">
<p>
<h4 class="header__download-box--subtitle">Try it in your browser!</h4>
<h4 class="header__download-box--subtitle">Use it in your browser!</h4>
<a href="#" class="download-button download-button__web"
title="what are you waiting for? click!"
onClick="window.open('https://sengi.nicolas-constant.com'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;"
@ -43,7 +43,7 @@
<br />
<br />
<h4 class="header__download-box--subtitle">Or download the desktop client:</h4>
<h4 class="header__download-box--subtitle">Or download the desktop client <span id="electron-version"></span>:</h4>
<div id="download-buttons" style="display: none;">
<a id="windows" href class="download-button" title="download client for windows">
<i class="fab fa-windows"></i>
@ -75,7 +75,7 @@
</div>
<div>
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi/releases/"
<a class="header__old-releases" href="https://github.com/NicolasConstant/sengi-electron/releases/"
title="browse previous releases">browse previous releases</a>
</div>
</div>
@ -215,6 +215,12 @@
return myJson;
}
const getLastElectronRelease = async () => {
const response = await fetch('https://api.github.com/repos/NicolasConstant/sengi-electron/releases/latest');
const myJson = await response.json();
return myJson;
}
function getOS() {
var userAgent = window.navigator.userAgent,
platform = window.navigator.platform,
@ -242,6 +248,9 @@
let lastRelease = await getLastRelease();
let version = lastRelease.tag_name;
let lastElectronRelease = await getLastElectronRelease();
let electronVersion = lastElectronRelease.tag_name;
var downloadButtons = document.getElementById('download-buttons');
downloadButtons.style.display = 'block';
@ -249,12 +258,15 @@
downloadButtonsNojs.style.display = 'none';
var sengiVersion = document.getElementById('sengi-version');
sengiVersion.textContent = `Current version: ${version}`;
sengiVersion.textContent = `Current version: v${version}`;
document.getElementById('windows').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-win.exe`;
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-mac.dmg`;
document.getElementById('deb').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-linux.deb`;
document.getElementById('appimage').href = `https://github.com/NicolasConstant/sengi/releases/download/${version}/Sengi-${version}-linux.AppImage`;
var htmlElectronVersion = document.getElementById('electron-version');
htmlElectronVersion.textContent = `(${electronVersion})`;
document.getElementById('windows').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-win.exe`;
document.getElementById('mac').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-mac.dmg`;
document.getElementById('deb').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-linux.deb`;
document.getElementById('appimage').href = `https://github.com/NicolasConstant/sengi-electron/releases/download/${electronVersion}/Sengi-${electronVersion.replace('v', '')}-linux.AppImage`;
let userOs = getOS();

View File

@ -11,4 +11,4 @@ include "mime-types.conf"
server.pid-file = "/run/lighttpd.pid"
index-file.names = ( "index.html", "index.htm" )
#url.rewrite-once = ( "^sengi/(.*)" => "/sengi/index.html" )
server.error-handler-404 = "/sengi/index.html"
server.error-handler-404 = "/index.html"

View File

@ -1,194 +0,0 @@
const { app, Menu, BrowserWindow, shell } = require("electron");
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 377,
height: 800,
title: "Sengi",
backgroundColor: "#131925",
useContentSize: true,
// webPreferences: {
// contextIsolation: true,
// nodeIntegration: false,
// nodeIntegrationInWorker: false
// }
});
win.setAutoHideMenuBar(true);
win.setMenuBarVisibility(false);
const sengiUrl = "https://sengi.nicolas-constant.com";
win.loadURL(sengiUrl);
const template = [
{
label: "View",
submenu: [
{
label: "Return on Sengi",
click() {
win.loadURL(sengiUrl);
}
},
{ type: "separator" },
{ role: "reload" },
{ role: "forcereload" },
{ type: 'separator' },
{ role: 'togglefullscreen' },
{ type: "separator" },
{ role: "close" },
{ role: 'quit' }
]
},
{
role: "help",
submenu: [
{ role: "toggledevtools" },
{
label: "Open GitHub project",
click() {
require("electron").shell.openExternal(
"https://github.com/NicolasConstant/sengi"
);
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
win.setMenu(menu);
// Check if we are on a MAC
if (process.platform === "darwin") {
// Create our menu entries so that we can use MAC shortcuts
Menu.setApplicationMenu(
Menu.buildFromTemplate([
{
label: "Sengi",
submenu: [
{ role: "close" },
{ role: 'quit' }
]
},
// {
// label: "File",
// submenu: [
// ]
// },
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "pasteandmatchstyle" },
{ role: "delete" },
{ role: "selectall" }
]
},
// {
// label: "Format",
// submenu: [
// ]
// },
{
label: "View",
submenu: [
{
label: "Return on Sengi",
click() {
win.loadURL(sengiUrl);
}
},
{ type: "separator" },
{ role: "reload" },
{ role: "forcereload" },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
// {
// label: "Window",
// submenu: [
// ]
// },
{
role: "Help",
submenu: [
{ role: "toggledevtools" },
{
label: "Open GitHub project",
click() {
require("electron").shell.openExternal(
"https://github.com/NicolasConstant/sengi"
);
}
}
]
}
])
);
}
//open external links to browser
win.webContents.on("new-window", function (event, url) {
event.preventDefault();
shell.openExternal(url);
});
// Emitted when the window is closed.
win.on("closed", () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
});
}
app.commandLine.appendSwitch("force-color-profile", "srgb");
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (win) {
if (win.isMinimized()) win.restore()
win.focus()
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", createWindow);
}
// Quit when all windows are closed.
app.on("window-all-closed", () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});

2871
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
{
"name": "sengi",
"version": "0.28.1",
"version": "1.7.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
"author": {
"name": "Nicolas Constant",
"name": "Nicolas Constant",
"email": "github@nicolas-constant.com"
},
"repository": {
@ -21,21 +21,18 @@
"test-nowatch": "ng test --watch=false",
"lint": "ng lint",
"e2e": "ng e2e",
"electron": "electron .",
"electron-prod": "ng build --prod && electron .",
"electron-debug": "ng build && electron .",
"dist": "npm run build && electron-builder --publish onTagOrDraft",
"travis": "electron-builder --publish onTagOrDraft"
"dist": "npm run build"
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.7",
"@angular/cdk": "^7.2.7",
"@angular/animations": "^7.2.16",
"@angular/cdk": "^7.3.7",
"@angular/common": "^7.2.7",
"@angular/compiler": "^7.2.7",
"@angular/core": "^7.2.7",
"@angular/forms": "^7.2.7",
"@angular/http": "^7.2.7",
"@angular/material": "^16.2.1",
"@angular/platform-browser": "^7.2.7",
"@angular/platform-browser-dynamic": "^7.2.7",
"@angular/pwa": "^0.12.4",
@ -47,9 +44,9 @@
"@fortawesome/free-brands-svg-icons": "^5.7.0",
"@fortawesome/free-regular-svg-icons": "^5.7.0",
"@fortawesome/free-solid-svg-icons": "^5.7.0",
"@ngxs/storage-plugin": "^3.2.0",
"@ngxs/store": "^3.2.0",
"angular2-hotkeys": "^2.1.5",
"@ngxs/storage-plugin": "~3.2.0",
"@ngxs/store": "~3.2.0",
"angular2-hotkeys": "~2.1.5",
"bootstrap": "^4.1.3",
"core-js": "^2.5.4",
"emojione": "~4.5.0",
@ -70,8 +67,6 @@
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.2.1",
"electron": "^8.0.2",
"electron-builder": "^20.39.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1",

View File

@ -16,6 +16,11 @@
</div>
</div>
<div *ngIf="enhancedTutorialActive" class="enhanced-tutorial"
[class.enhanced-tutorial__visible]="enhancedTutorialVisible">
<app-tutorial-enhanced class="enhanced-tutorial__content" (closeEvent)="closeTutorial()"></app-tutorial-enhanced>
</div>
<app-media-viewer id="media-viewer" *ngIf="openedMediaEvent" [openedMediaEvent]="openedMediaEvent"
(closeSubject)="closeMedia()" (dragenter)="dragenter($event)"></app-media-viewer>

View File

@ -171,4 +171,27 @@ app-streams-selection-footer {
}
}
}
}
.enhanced-tutorial {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.9);
z-index: 9999999;
opacity: 0;
transition: all .4s;
&__visible {
opacity: 1;
}
&__content {
display: block;
padding: 25px;
width: calc(100%);
height: calc(100%);
}
}

View File

@ -62,9 +62,6 @@ export class AppComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
// disable tutorial for future update
localStorage.setItem('tutorial', JSON.stringify(true));
this.paramsSub = this.activatedRoute.queryParams.subscribe(params => {
const code = params['code'];
if (!code) {
@ -130,6 +127,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((event: OpenLeftPanelEvent) => {
if (event.type === LeftPanelType.Closed) {
this.floatingColumnActive = false;
this.checkEnhancedTutorial();
} else {
this.floatingColumnActive = true;
}
@ -158,6 +157,29 @@ export class AppComponent implements OnInit, OnDestroy {
});
}
enhancedTutorialActive: boolean;
enhancedTutorialVisible: boolean;
private checkEnhancedTutorial() {
let enhancedTutorialDesactivated = JSON.parse(localStorage.getItem('tutorial'));
if (!this.floatingColumnActive && !this.tutorialActive && !enhancedTutorialDesactivated) {
setTimeout(() => {
this.enhancedTutorialActive = true;
setTimeout(() => {
this.enhancedTutorialVisible = true;
}, 100);
}, 500);
}
}
closeTutorial(){
localStorage.setItem('tutorial', JSON.stringify(true));
this.enhancedTutorialVisible = false;
setTimeout(() => {
this.enhancedTutorialActive = false;
}, 400);
}
ngOnDestroy(): void {
this.streamSub.unsubscribe();
this.columnEditorSub.unsubscribe();

View File

@ -5,8 +5,8 @@ import { HttpModule } from "@angular/http";
import { HttpClientModule } from '@angular/common/http';
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
// import { NgxElectronModule } from "ngx-electron";
import { DragDropModule } from '@angular/cdk/drag-drop';
// import { NgxElectronModule } from 'ngx-electron';
import { NgxsModule } from '@ngxs/store';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
@ -83,7 +83,14 @@ import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { BookmarksComponent } from './components/floating-column/manage-account/bookmarks/bookmarks.component';
import { AttachementImageComponent } from './components/stream/status/attachements/attachement-image/attachement-image.component';
import { EnsureHttpsPipe } from './pipes/ensure-https.pipe';
import { UserFollowsComponent } from './components/stream/user-follows/user-follows.component';
import { AccountComponent } from './components/common/account/account.component';
import { TutorialEnhancedComponent } from './components/tutorial-enhanced/tutorial-enhanced.component';
import { NotificationsTutorialComponent } from './components/tutorial-enhanced/notifications-tutorial/notifications-tutorial.component';
import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-tutorial.component';
import { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component';
import { StatusTranslateComponent } from './components/stream/status/status-translate/status-translate.component';
const routes: Routes = [
{ path: "", component: StreamsMainDisplayComponent },
@ -146,7 +153,15 @@ const routes: Routes = [
StreamNotificationsComponent,
NotificationComponent,
BookmarksComponent,
AttachementImageComponent
AttachementImageComponent,
EnsureHttpsPipe,
UserFollowsComponent,
AccountComponent,
TutorialEnhancedComponent,
NotificationsTutorialComponent,
LabelsTutorialComponent,
ThankyouTutorialComponent,
StatusTranslateComponent
],
entryComponents: [
EmojiPickerComponent
@ -160,9 +175,11 @@ const routes: Routes = [
FormsModule,
ReactiveFormsModule,
PickerModule,
OwlDateTimeModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
OverlayModule,
DragDropModule,
// NgxElectronModule,
RouterModule.forRoot(routes),
NgxsModule.forRoot([

View File

@ -0,0 +1,5 @@
<a href class="account" title="open account" (click)="selected()" (auxclick)="openAccount()">
<img src="{{account.avatar}}" class="account__avatar" />
<div class="account__name" innerHTML="{{ account | accountEmoji }}"></div>
<div class="account__fullhandle">@{{ account.acct }}</div>
</a>

View File

@ -0,0 +1,48 @@
@import "variables";
@import "mixins";
.account {
font-size: $small-font-size;
display: block;
color: white;
border-radius: 2px;
transition: all .3s;
border-top: 1px solid $separator-color;
overflow: hidden;
&:last-of-type {
border-bottom: 1px solid $separator-color;
}
&__avatar {
width: 40px;
margin: 5px 10px 5px 5px;
float: left;
border-radius: 2px;
}
&__name {
margin: 7px 0 0 0;
}
&__fullhandle {
margin: 0 0 5px 0;
color: $status-secondary-color;
transition: all .3s;
white-space: nowrap;
}
&:hover,
&:hover &__fullhandle {
color: white;
text-decoration: none;
}
&:hover {
background-color: $button-background-color-hover;
}
@include clearfix;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountComponent } from './account.component';
xdescribe('AccountComponent', () => {
let component: AccountComponent;
let fixture: ComponentFixture<AccountComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AccountComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AccountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,29 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { Account } from '../../../services/models/mastodon.interfaces';
@Component({
selector: 'app-account',
templateUrl: './account.component.html',
styleUrls: ['./account.component.scss']
})
export class AccountComponent implements OnInit {
@Input() account: Account;
@Output() accountSelected = new EventEmitter<Account>();
constructor() { }
ngOnInit() {
}
selected(): boolean{
this.accountSelected.next(this.account);
return false;
}
openAccount(): boolean {
window.open(this.account.url, '_blank');
return false;
}
}

View File

@ -0,0 +1,25 @@
import { Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { OpenThreadEvent } from '../../services/tools.service';
export abstract class BrowseBase implements OnInit, OnDestroy {
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
abstract ngOnInit();
abstract ngOnDestroy();
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -0,0 +1,123 @@
import { OnInit, Input, OnDestroy, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
import { AccountInfo } from '../../states/accounts.state';
import { StreamingWrapper } from '../../services/streaming.service';
import { NotificationService } from '../../services/notification.service';
import { ToolsService, OpenThreadEvent } from '../../services/tools.service';
import { StatusWrapper } from '../../models/common.model';
import { Status } from '../../services/models/mastodon.interfaces';
import { TimeLineModeEnum } from '../../states/settings.state';
import { BrowseBase } from './browse-base';
export abstract class TimelineBase extends BrowseBase {
isLoading = true;
protected maxReached = false;
protected lastCallReachedMax = false;
isThread = false;
displayError: string;
hasContentWarnings = false;
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
protected account: AccountInfo;
protected websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = [];
bufferStream: Status[] = [];
protected bufferWasCleared: boolean;
numNewItems: number;
streamPositionnedAtTop: boolean = true;
protected isProcessingInfiniteScroll: boolean;
protected hideBoosts: boolean;
protected hideReplies: boolean;
protected hideBots: boolean;
@Input() goToTop: Observable<void>;
@Input() userLocked = true;
@ViewChild('statusstream') public statustream: ElementRef;
constructor(
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super();
}
abstract ngOnInit();
abstract ngOnDestroy();
protected abstract scrolledToTop();
protected abstract statusProcessOnGoToTop();
protected abstract getNextStatuses(): Promise<Status[]>;
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
const atTop = element.scrollTop === 0;
this.streamPositionnedAtTop = false;
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
} else if (atTop) {
this.scrolledToTop();
}
}
private scrolledErrorOccured = false;
protected scrolledToBottom() {
if (this.isLoading || this.maxReached || this.scrolledErrorOccured) return;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
this.getNextStatuses()
.then((status: Status[]) => {
if (!status || status.length === 0 || this.maxReached) {
this.maxReached = true;
return;
}
if (status) {
for (const s of status) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
}
if(this.lastCallReachedMax){
this.maxReached = true;
}
})
.catch((err: HttpErrorResponse) => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
});
}
applyGoToTop(): boolean {
this.statusProcessOnGoToTop();
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
}

View File

@ -1,14 +1,23 @@
<form class="status-editor" (ngSubmit)="onSubmit()">
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" />
<input #mytitle [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title"
name="title" autocomplete="off" placeholder="Title, Content Warning (optional)"
title="title, content warning (optional)" dir="auto"
(keydown.escape)="mytitle.blur()" />
<a class="status-editor__emoji" title="Insert Emoji"
#emojiButton href (click)="openEmojiPicker($event)">
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
</a>
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
rows="5" required title="content" placeholder="What's on your mind?" (keydown.control.enter)="onCtrlEnter()"
<a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
{{ selectedLanguage.iso639 }}
</a>
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
rows="5" required title="content" placeholder="What's on your mind?"
(keydown.control.enter)="onCtrlEnter()"
(keydown.meta.enter)="onCtrlEnter()"
(keydown.escape)="reply.blur()"
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
</textarea>
@ -21,19 +30,21 @@
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
</app-autosuggest>
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive"></app-poll-editor>
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive" [oldPoll]="oldPoll"></app-poll-editor>
<app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler>
<div class="status-editor__footer" #footer>
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
<span *ngIf="!isSending && !scheduleIsActive">REPLY!</span>
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">REPLY!</span>
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
<span *ngIf="!isSending && isEditing">EDIT!</span>
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
</button>
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
<span *ngIf="!isSending && !scheduleIsActive">POST!</span>
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">POST!</span>
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
<span *ngIf="!isSending && isEditing">EDIT!</span>
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
</button>
<div class="status-editor__footer__counter">
@ -64,6 +75,10 @@
<fa-icon [icon]="faClock"></fa-icon>
</a>
</div>
<div class="language-warning" *ngIf="!configuredLanguages || configuredLanguages.length === 0">
You haven't set your language(s) yet, please <a href class="language-warning__link" (click)="onNavigateToSettings()">go in the settings</a> to provide it.
</div>
<context-menu #contextMenu>
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
@ -79,5 +94,12 @@
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
</ng-template>
</context-menu>
<context-menu #langContextMenu>
<ng-template contextMenuItem (execute)="setLanguage(l)" *ngFor="let l of configuredLanguages">
{{ l.name }}
</ng-template>
</context-menu>
<app-media></app-media>
</form>

View File

@ -70,6 +70,32 @@ $counter-width: 90px;
}
}
&__lang {
position: absolute;
top: 64px;
right: 12px;
font-weight: bolder;
font-size: 12px;
color: #a5a5a5;
text-decoration: none;
display: block;
width: 20px;
height: 19px;
border-radius: 2px;
background-color: rgba(255, 255, 255, 0);
padding: 1px 0 0 2px;
text-transform: uppercase;
&:hover {
text-decoration: none;
color:black;
background-color: #e6e6e6;
}
}
&__content {
border-width: 0;
background-color: $status-editor-background;
@ -154,6 +180,9 @@ $counter-width: 90px;
}
& span {
position: relative;
top: 1px;
margin: 0;
padding: 0;
}
@ -204,6 +233,20 @@ $counter-width: 90px;
border-bottom: 1px solid whitesmoke;
}
.language-warning {
padding: 5px 10px;
color: orange;
&__link {
text-decoration: underline;
color: #f0d124;
&:hover {
color: #d18800;
}
}
}
@import '~@angular/cdk/overlay-prebuilt.css';
// ::ng-deep .cdk-overlay-backdrop {
// // width: 100%;

View File

@ -15,7 +15,7 @@ import { NavigationService } from '../../services/navigation.service';
import { NotificationService } from '../../services/notification.service';
import { MastodonService } from '../../services/mastodon.service';
import { AuthService } from '../../services/auth.service';
import { SettingsState } from '../../states/settings.state';
describe('CreateStatusComponent', () => {
let component: CreateStatusComponent;
@ -33,7 +33,8 @@ describe('CreateStatusComponent', () => {
NgxsModule.forRoot([
RegisteredAppsState,
AccountsState,
StreamsState
StreamsState,
SettingsState
]),
],
providers: [NavigationService, NotificationService, MastodonService, AuthService],
@ -164,6 +165,41 @@ describe('CreateStatusComponent', () => {
expect(result.length).toBe(1);
});
it('should cound URL correctly', () => {
const newLine = String.fromCharCode(13, 10);
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s`;
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(0);
});
it('should cound URL correctly - new lines', () => {
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd\nhttps://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs\ndsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s`;
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(0);
});
it('should cound URL correctly - dual post', () => {
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s`;
(<any>component).maxCharLength = 512;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(0);
expect((<any>component).postCounts).toBe(2);
});
it('should cound URL correctly - triple post', () => {
const status = `qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd https://google.com/testqsdqsdqsdqsdqsdqsdqsdqdqsdqsdqsdqsdqs dsqd qsd qsd dsqdqs dqs dqsd qsd qsd qsd qsd qsd qs dqsdsq qsd qsd qs dsqds qqs d dqs dqs dqs dqqsd qsd qsd qsd sqd qsd qsd sqd qds dsqd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd qsd qsd qs dqsd qsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsd qsd qsddq sqd qsd qsdqs dqsd qsd qsd qsd qsd qsd qsd dsqd qsd qsd dsqdqs fqd dsq sq dsq qsd q qsd qsd qs dqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsqs dqs qsd qsd qss sq ss s dqsd qsd sqd qsqsd qsd qsdd dqsd qs s dsqs sd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd qs qsd qsd qsd qsd sqd qsd qsd sqd qsd qsd qsd qsd qsd qsd qsd qsd qsd qsd sd`;
(<any>component).maxCharLength = 512;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(0);
expect((<any>component).postCounts).toBe(3);
});
it('should add alias in multiposting replies', () => {
const status = '@Lorem@ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictu0';
(<any>component).maxCharLength = 500;
@ -223,4 +259,79 @@ describe('CreateStatusComponent', () => {
const result = <string>(<any>component).tranformHtmlRepliesToReplies(pleromaMention);
expect(result).toBe('<p>test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>');
});
it('should autocomplete - at the end', () => {
let text = 'data @sengi';
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe('data @sengi@mastodon.social ');
});
it('should autocomplete - at the start', () => {
let text = '@sengi data';
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe('@sengi@mastodon.social data');
});
it('should autocomplete - at the middle', () => {
let text = 'data @sengi data';
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe('data @sengi@mastodon.social data');
});
it('should autocomplete - duplicate', () => {
let text = 'data @sengi @sengi2 data';
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe('data @sengi@mastodon.social @sengi2 data');
});
it('should autocomplete - duplicate 2', () => {
let text = 'data @sengi2 @sengi data';
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe('data @sengi2 @sengi@mastodon.social data');
});
it('should autocomplete - new lines', () => {
const newLine = String.fromCharCode(13, 10);
let text = `@sengi${newLine}${newLine}data`;
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe(`@sengi@mastodon.social${newLine}${newLine}data`);
});
it('should autocomplete - new lines 2', () => {
const newLine = String.fromCharCode(13, 10);
let text = `@nicolasconstant\n\ndata`;
let pattern = '@nicolasconstant';
let autosuggest = '@nicolasconstant@social.nicolas-constant.com';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe(`@nicolasconstant@social.nicolas-constant.com${newLine}${newLine}data`);
});
it('should autocomplete - complex', () => {
const newLine = String.fromCharCode(13, 10);
let text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ullamcorper nulla eu metus euismod, non lobortis${newLine}quam congue. @sengi Ut hendrerit, nulla vel feugiat lobortis, diam ligula congue lacus, sed facilisis nisl dui at mauris.${newLine}Cras non hendrerit tellus. Donec eleifend metus quis nibh commodo${newLine}${newLine}data`;
let pattern = '@sengi';
let autosuggest = '@sengi@mastodon.social';
const result = <string>(<any>component).replacePatternWithAutosuggest(text, pattern, autosuggest);
expect(result).toBe(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ullamcorper nulla eu metus euismod, non lobortis${newLine}quam congue. @sengi@mastodon.social Ut hendrerit, nulla vel feugiat lobortis, diam ligula congue lacus, sed facilisis nisl dui at mauris.${newLine}Cras non hendrerit tellus. Donec eleifend metus quis nibh commodo${newLine}${newLine}data`);
});
});

View File

@ -11,7 +11,7 @@ import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
import { Status, Attachment } from '../../services/models/mastodon.interfaces';
import { Status, Attachment, Poll } from '../../services/models/mastodon.interfaces';
import { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service';
import { NotificationService } from '../../services/notification.service';
import { StatusWrapper } from '../../models/common.model';
@ -24,6 +24,10 @@ import { PollEditorComponent } from './poll-editor/poll-editor.component';
import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.component';
import { ScheduledStatusService } from '../../services/scheduled-status.service';
import { StatusesStateService } from '../../services/statuses-state.service';
import { SettingsService } from '../../services/settings.service';
import { LanguageService } from '../../services/language.service';
import { ILanguage } from '../../states/settings.state';
import { LeftPanelType, NavigationService } from '../../services/navigation.service';
@Component({
selector: 'app-create-status',
@ -41,6 +45,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
faClock = faClock;
autoSuggestUserActionsStream = new EventEmitter<AutosuggestUserActionEnum>();
private isRedrafting: boolean;
private _title: string;
set title(value: string) {
@ -54,11 +59,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private _status: string = '';
@Input('status')
set status(value: string) {
this.statusStateService.setStatusContent(value, this.statusReplyingToWrapper);
if (this.isRedrafting) {
this.statusStateService.setStatusContent(value, null);
} else {
this.statusStateService.setStatusContent(value, this.statusReplyingToWrapper);
}
this.countStatusChar(value);
this.detectAutosuggestion(value);
this._status = value;
this.languageService.autoDetectLang(value);
setTimeout(() => {
this.autoGrow();
}, 0);
@ -77,33 +88,51 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return s;
}
@Input('statusToEdit')
set statusToEdit(value: StatusWrapper) {
if (value) {
this.isEditing = true;
this.editingStatusId = value.status.id;
this.redraftedStatus = value;
this.mediaService.loadMedia(value.status.media_attachments);
}
}
@Input('redraftedStatus')
set redraftedStatus(value: StatusWrapper) {
if (value) {
if (value) {
this.isRedrafting = true;
this.statusLoaded = false;
if(value.status && value.status.media_attachments){
if (value.status && value.status.media_attachments) {
for (const m of value.status.media_attachments) {
this.mediaService.addExistingMedia(new MediaWrapper(m.id, null, m));
}
}
const newLine = String.fromCharCode(13, 10);
let content = value.status.content;
content = this.tranformHtmlRepliesToReplies(content);
while (content.includes('<p>') || content.includes('</p>') || content.includes('<br>') || content.includes('<br/>') || content.includes('<br />')) {
content = content.replace('<p>', '').replace('</p>', newLine + newLine).replace('<br />', newLine).replace('<br/>', newLine).replace('<br>', newLine);
}
content = this.trim(content, newLine);
let parser = new DOMParser();
var dom = parser.parseFromString(content, 'text/html')
this.status = dom.body.textContent;
this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
// this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
// Retrieve mentions
for(let mention of value.status.mentions){
if(this.status){
this.status = this.status.replace(`@${mention.username}`, `@${mention.acct}`);
}
}
this.setVisibilityFromStatus(value.status);
this.title = value.status.spoiler_text;
@ -123,9 +152,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.isSending = false;
});
}
if(value.status.poll){
this.pollIsActive = true;
this.oldPoll = value.status.poll;
// setTimeout(() => {
// if(this.pollEditor) this.pollEditor.loadPollParameters(value.status.poll);
// }, 250);
}
}
}
oldPoll: Poll;
private maxCharLength: number;
charCountLeft: number;
postCounts: number = 1;
@ -134,6 +173,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
autosuggestData: string = null;
instanceSupportsPoll = true;
instanceSupportsScheduling = true;
isEditing: boolean;
editingStatusId: string;
configuredLanguages: ILanguage[] = [];
selectedLanguage: ILanguage;
private statusLoaded: boolean;
private hasSuggestions: boolean;
@ -143,6 +186,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
@ViewChild('fileInput') fileInputElement: ElementRef;
@ViewChild('footer') footerElement: ElementRef;
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
@ViewChild('langContextMenu') public langContextMenu: ContextMenuComponent;
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
@ -173,14 +217,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private statusReplyingTo: Status;
selectedPrivacy = 'Public';
// privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
private selectedPrivacySetByRedraft = false;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
private langSub: Subscription;
private selectLangSub: Subscription;
private selectedAccount: AccountInfo;
constructor(
private statusStateService: StatusesStateService,
private readonly navigationService: NavigationService,
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService,
private readonly statusStateService: StatusesStateService,
private readonly scheduledStatusService: ScheduledStatusService,
private readonly contextMenuService: ContextMenuService,
private readonly store: Store,
@ -190,13 +239,45 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private readonly instancesInfoService: InstancesInfoService,
private readonly mediaService: MediaService,
private readonly overlay: Overlay,
public viewContainerRef: ViewContainerRef) {
public viewContainerRef: ViewContainerRef,
private readonly statusesStateService: StatusesStateService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
}
private initLanguages(){
this.configuredLanguages = this.languageService.getConfiguredLanguages();
this.selectedLanguage = this.languageService.getSelectedLanguage();
this.langSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
this.configuredLanguages = l;
// if(this.configuredLanguages.length > 0
// && this.selectedLanguage
// && this.configuredLanguages.findIndex(x => x.iso639 === this.selectedLanguage.iso639)){
// this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
// }
});
this.selectLangSub = this.languageService.selectedLanguageChanged.subscribe(l => {
this.selectedLanguage = l;
});
if(!this.selectedLanguage && this.configuredLanguages.length > 0){
this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
}
}
setLanguage(lang: ILanguage): boolean {
if(lang){
this.languageService.setSelectedLanguage(lang);
}
return false;
}
ngOnInit() {
this.initLanguages();
if (!this.isRedrafting) {
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
}
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.accountChanged(accounts);
});
@ -209,11 +290,12 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.statusReplyingTo = this.statusReplyingToWrapper.status;
}
let state = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
if (state && state !== '') {
this.status = state;
} else {
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
// let state = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
// if (state && state !== '') {
// this.status = state;
// } else {
if (!this.status || this.status === '') {
const uniqueMentions = this.getMentions(this.statusReplyingTo);
for (const mention of uniqueMentions) {
this.status += `@${mention} `;
}
@ -233,7 +315,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
if (this.isRedrafting) {
this.statusStateService.resetStatusContent(null);
}
this.accountSub.unsubscribe();
this.langSub.unsubscribe();
this.selectLangSub.unsubscribe();
}
onNavigateToSettings(): boolean {
this.navigationService.openPanel(LeftPanelType.Settings);
return false;
}
onPaste(e: any) {
@ -292,7 +385,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
};
const word = this.getWordByPos(currentSection, caretPosition - offset);
if (!lastCharIsSpace && word && word.length > 0 && (word.startsWith('@') || word.startsWith('#'))) {
if (!lastCharIsSpace && word && word.length > 1 && (word.startsWith('@') || word.startsWith('#'))) {
this.autosuggestData = word;
return;
}
@ -356,7 +449,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
if (accounts && accounts.length > 0) {
this.selectedAccount = accounts.filter(x => x.isSelected)[0];
const settings = this.toolsService.getAccountSettings(this.selectedAccount);
const settings = this.settingsService.getAccountSettings(this.selectedAccount);
if (settings.customStatusCharLengthEnabled) {
this.maxCharLength = settings.customStatusCharLength;
this.countStatusChar(this.status);
@ -415,9 +508,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.setVisibility(VisibilityEnum.Direct);
break;
}
this.selectedPrivacySetByRedraft = true;
}
private setVisibility(defaultPrivacy: VisibilityEnum) {
if (this.selectedPrivacySetByRedraft) return;
switch (defaultPrivacy) {
case VisibilityEnum.Public:
this.selectedPrivacy = 'Public';
@ -456,8 +553,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
const currentStatus = parseStatus[parseStatus.length - 1];
const statusExtraChars = this.getMentionExtraChars(status);
const linksExtraChars = this.getLinksExtraChars(status);
const statusExtraChars = this.getMentionExtraChars(currentStatus);
const linksExtraChars = this.getLinksExtraChars(currentStatus);
const statusLength = [...currentStatus].length - statusExtraChars - linksExtraChars;
this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
@ -472,8 +569,20 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return cwLength;
}
private getMentions(status: Status, providerInfo: AccountInfo): string[] {
const mentions = [status.account.acct, ...status.mentions.map(x => x.acct)];
private getMentions(status: Status): string[] {
let acct = status.account.acct;
if (!acct.includes('@')) {
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
}
const mentions = [acct];
status.mentions.forEach(m => {
let mentionAcct = m.acct;
if (!mentionAcct.includes('@')) {
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
}
mentions.push(mentionAcct);
});
let uniqueMentions = [];
for (let mention of mentions) {
@ -482,22 +591,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
}
let globalUniqueMentions = [];
for (let mention of uniqueMentions) {
if (!mention.includes('@')) {
if (providerInfo) {
mention += `@${providerInfo.instance}`;
} else {
mention += `@${status.url.replace('https://', '').split('/')[0]}`;
}
}
globalUniqueMentions.push(mention);
}
const selectedUser = this.toolsService.getSelectedAccounts()[0];
globalUniqueMentions = globalUniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
uniqueMentions = uniqueMentions.filter(x => x.toLowerCase() !== `${selectedUser.username}@${selectedUser.instance}`.toLowerCase());
return globalUniqueMentions;
return uniqueMentions;
}
onCtrlEnter(): boolean {
@ -505,7 +602,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return false;
}
onSubmit(): boolean {
async onSubmit(): Promise<boolean> {
if (this.isSending || this.mentionTooFarAwayError) return false;
this.isSending = true;
@ -526,9 +623,10 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
break;
}
const mediaAttachments = this.mediaService.mediaSubject.value.map(x => x.attachment);
const acc = this.toolsService.getSelectedAccounts()[0];
const mediaAttachments = (await this.mediaService.retrieveUpToDateMedia(acc)).map(x => x.attachment);
let usableStatus: Promise<Status>;
if (this.statusReplyingToWrapper) {
usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
@ -552,7 +650,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
usableStatus
.then((status: Status) => {
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime);
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime, this.editingStatusId);
})
.then((res: Status) => {
this.title = '';
@ -563,7 +661,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.scheduledStatusService.statusAdded(acc);
}
this.statusStateService.resetStatusContent(this.statusReplyingToWrapper);
if (this.isRedrafting) {
this.statusStateService.resetStatusContent(null);
} else {
this.statusStateService.resetStatusContent(this.statusReplyingToWrapper);
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, acc);
@ -575,7 +677,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return false;
}
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise<Status> {
private currentLang(): string {
if(this.selectedLanguage){
return this.selectedLanguage.iso639;
}
return null;
}
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string, editingStatusId: string): Promise<Status> {
let parsedStatus = this.parseStatus(status);
let resultPromise = Promise.resolve(previousStatus);
@ -589,13 +699,25 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
if (i === 0) {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt)
let postPromise: Promise<Status>;
if (this.isEditing) {
postPromise = this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, attachments, poll, scheduledAt, this.currentLang());
} else {
postPromise = this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt, this.currentLang());
}
return postPromise
.then((status: Status) => {
this.mediaService.clearMedia();
return status;
});
} else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
if (this.isEditing) {
return this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
} else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
}
}
})
.then((status: Status) => {
@ -604,6 +726,16 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(cwPolicy.status, account, cwPolicy.applyCw, cwPolicy.hide));
}
return status;
})
.then((status: Status) => {
if (this.isEditing) {
let cwPolicy = this.toolsService.checkContentWarning(status);
let statusWrapper = new StatusWrapper(status, account, cwPolicy.applyCw, cwPolicy.hide);
this.statusesStateService.statusEditedStatusChanged(status.url, account.id, statusWrapper);
}
return status;
});
}
@ -613,6 +745,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private parseStatus(status: string): string[] {
let mentionExtraChars = this.getMentionExtraChars(status);
let urlExtraChar = this.getLinksExtraChars(status);
let trucatedStatus = `${status}`;
let results = [];
@ -622,13 +755,24 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
aggregateMention += `${x} `;
});
const currentMaxCharLength = this.maxCharLength + mentionExtraChars - this.getCwLength();
const maxChars = currentMaxCharLength - 6;
let currentMaxCharLength = this.maxCharLength + mentionExtraChars + urlExtraChar - this.getCwLength();
let maxChars = currentMaxCharLength - 6;
while (trucatedStatus.length > currentMaxCharLength) {
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
if (nextIndex === -1) {
break;
}
results.push(trucatedStatus.substr(0, nextIndex) + ' (...)');
trucatedStatus = aggregateMention + trucatedStatus.substr(nextIndex + 1);
// Refresh max
let mentionExtraChars = this.getMentionExtraChars(trucatedStatus);
let urlExtraChar = this.getLinksExtraChars(trucatedStatus);
currentMaxCharLength = this.maxCharLength + mentionExtraChars + urlExtraChar - this.getCwLength();
maxChars = currentMaxCharLength - 6;
}
results.push(trucatedStatus);
return results;
@ -636,7 +780,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private getLinksExtraChars(status: string): number {
let mentionExtraChars = 0;
let links = status.split(' ').filter(x => x.startsWith('http://') || x.startsWith('https://'));
let links = status.split(/\s+/).filter(x => x.startsWith('http://') || x.startsWith('https://'));
for (let link of links) {
if (link.length > 23) {
mentionExtraChars += link.length - 23;
@ -667,15 +811,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
suggestionSelected(selection: AutosuggestSelection) {
if (this.status.includes(selection.pattern)) {
this.status = this.replacePatternWithAutosuggest(this.status, selection.pattern, selection.autosuggest);
let transformedStatus = this.status;
transformedStatus = transformedStatus.replace(new RegExp(` ${selection.pattern} `), ` ${selection.autosuggest} `).replace(' ', ' ');
transformedStatus = transformedStatus.replace(new RegExp(`${selection.pattern} `), `${selection.autosuggest} `).replace(' ', ' ');
transformedStatus = transformedStatus.replace(new RegExp(`${selection.pattern}$`), `${selection.autosuggest} `).replace(' ', ' ');
this.status = transformedStatus;
let newCaretPosition = this.status.indexOf(`${selection.autosuggest} `) + selection.autosuggest.length + 1;
if (newCaretPosition > this.status.length) newCaretPosition = this.status.length;
let cleanStatus = this.status.replace(/\r?\n/g, ' ');
let newCaretPosition = cleanStatus.indexOf(`${selection.autosuggest}`) + selection.autosuggest.length;
if (newCaretPosition > cleanStatus.length) newCaretPosition = cleanStatus.length;
this.autosuggestData = null;
this.hasSuggestions = false;
@ -690,6 +830,57 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
}
private replacePatternWithAutosuggest(status: string, pattern: string, autosuggest: string): string {
status = status.replace(/ /g, ' ');
const newLine = String.fromCharCode(13, 10);
// let statusPerLines = status.split(newLine);
let statusPerLines = status.split(/\r?\n/);
let statusPerLinesPerWords: string[][] = [];
let regex = new RegExp(`^${pattern}$`, 'i');
statusPerLines.forEach(line => {
let words = line.split(' ');
words = words.map(word => {
return word.replace(regex, `${autosuggest}`);
});
statusPerLinesPerWords.push(words);
});
let result = '';
let nberLines = statusPerLinesPerWords.length;
let i = 0;
statusPerLinesPerWords.forEach(line => {
i++;
let wordCount = line.length;
let w = 0;
line.forEach(word => {
w++;
result += `${word}`;
if (w < wordCount || i === nberLines) {
result += ' ';
}
});
if (i < nberLines) {
result += newLine;
}
})
result = result.replace(' ', ' ');
let endRegex = new RegExp(`${autosuggest} $`, 'i');
if (!result.match(endRegex)) {
result = result.substring(0, result.length - 1);
}
return result;
}
suggestionsChanged(hasSuggestions: boolean) {
this.hasSuggestions = hasSuggestions;
}
@ -741,9 +932,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
if (isVisible) {
setTimeout(() => {
try{
try {
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
}catch(err) {
} catch (err) {
this.footerElement.nativeElement.scrollIntoView({ behavior: 'instant', block: 'end', inline: 'start' });
}
}, 0);
@ -768,6 +959,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
$event.stopPropagation();
}
public onLangContextMenu($event: MouseEvent): void {
this.contextMenuService.show.next({
// Optional - if unspecified, all context menu components will open
contextMenu: this.langContextMenu,
event: $event,
item: null
});
$event.preventDefault();
$event.stopPropagation();
}
//https://stackblitz.com/edit/overlay-demo
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
overlayRef: OverlayRef;
@ -800,7 +1002,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.overlayRef = this.overlay.create(config);
// this.overlayRef.backdropClick().subscribe(() => {
// console.warn('wut?');
// this.overlayRef.dispose();
// });
@ -850,11 +1051,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
const mastodonMentionRegex = /<span class="h-card"><a href="https:\/\/([a-zA-Z0-9.]{0,255})\/[a-zA-Z0-9_@/-]{0,255}" class="u-url mention">@<span>([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi;
const pleromaMentionRegex = /<span class="h-card"><a data-user="[a-zA-Z0-9]{0,255}" class="u-url mention" href="https:\/\/([a-zA-Z0-9.]{0,255})\/[a-zA-Z0-9_@/-]{0,255}" rel="ugc">@<span>([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi;
while(data.match(mastodonMentionRegex)){
while (data.match(mastodonMentionRegex)) {
data = data.replace(mastodonMentionRegex, '@$2@$1');
}
while(data.match(pleromaMentionRegex)){
while (data.match(pleromaMentionRegex)) {
data = data.replace(pleromaMentionRegex, '@$2@$1');
}

View File

@ -1,8 +1,8 @@
<div *ngFor="let m of media" class="media">
<div *ngIf="m.attachment === null" class="media__loading" title="{{m.file.name}}">
<div *ngIf="m.attachment === null" class="media__loading" title="{{getName(m)}}">
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
</div>
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{m.file.name}}"
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" class="media__loaded" title="{{getName(m)}}"
(mouseleave)="updateMedia(m)">
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
<app-waiting-animation class="waiting-icon"></app-waiting-animation>

View File

@ -56,4 +56,13 @@ export class MediaComponent implements OnInit, OnDestroy {
this.mediaService.update(account, media);
return false;
}
getName(media: MediaWrapper): string {
if(media && media.file && media.file.name){
return media.file.name;
}
if(media.attachment && media.attachment.description){
return media.attachment.description;
}
}
}

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Component, Input, OnInit, SimpleChanges } from '@angular/core';
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { PollEntry } from './poll-entry/poll-entry.component';
import { PollParameters } from '../../../services/mastodon.service';
import { retry } from 'rxjs/operators';
import { Poll } from '../../../services/models/mastodon.interfaces';
@Component({
selector: 'app-poll-editor',
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
selectedId: string;
private multiSelected: boolean;
@Input() oldPoll: Poll;
constructor() {
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
@ -40,6 +42,12 @@ export class PollEditorComponent implements OnInit {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['oldPoll']) {
this.loadPollParameters(this.oldPoll);
}
}
private getEntryUuid(): number {
this.entryUuid++;
return this.entryUuid;
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
return false;
}
removeElement(entry: PollEntry){
removeElement(entry: PollEntry) {
this.entries = this.entries.filter(x => x.id != entry.id);
}
@ -69,6 +77,19 @@ export class PollEditorComponent implements OnInit {
params.hide_totals = false;
return params;
}
private loadPollParameters(poll: Poll) {
if(!this.oldPoll) return;
const isMulti = poll.multiple;
this.entries.length = 0;
for (let o of poll.options) {
const entry = new PollEntry(this.getEntryUuid(), isMulti);
entry.label = o.title;
this.entries.push(entry);
}
}
}
class Delay {

View File

@ -15,7 +15,9 @@
<button type="submit" class="form-button"
title="add account"
[class.comrade__button]="isComrade">
<span *ngIf="!isLoading">Submit</span>
<span *ngIf="!isLoading && !this.isInstanceMultiAccountLoading">Submit</span>
<span *ngIf="!isLoading && this.isInstanceMultiAccountLoading" class="faq__warning">See FAQ</span>
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
</button>
@ -29,5 +31,12 @@
allowfullscreen></iframe>
</div>
<div class="faq" *ngIf="isInstanceMultiAccount">
<p>
FAQ<br/>
<a href="https://github.com/NicolasConstant/sengi/wiki/How-to-add-multiple-accounts-from-the-same-instance" target="_blank">How to add multiple accounts from the same instance?</a>
</p>
</div>
</div>
</div>

View File

@ -109,4 +109,21 @@ $comrade_red: #a50000;
background-color: $comrade_red;
background-position: 0 0;
}
}
.faq {
margin: 20px 0 0 0;
& a {
color: #ffcc00;
text-decoration: underline;
&:hover {
color: #ffe88a;
}
}
&__warning {
color: #ffdc52;
}
}

View File

@ -6,13 +6,14 @@ import { RegisteredAppsStateModel, AppInfo, AddRegisteredApp } from '../../../st
import { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
import { AppData } from '../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../services/notification.service';
import { ToolsService } from '../../../services/tools.service';
@Component({
selector: 'app-add-new-account',
templateUrl: './add-new-account.component.html',
styleUrls: ['./add-new-account.component.scss']
})
export class AddNewAccountComponent implements OnInit {
export class AddNewAccountComponent implements OnInit {
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
private comradeList = ['juche.town'];
@ -24,12 +25,14 @@ export class AddNewAccountComponent implements OnInit {
set setInstance(value: string) {
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
this.checkComrad();
this.checkInstanceMultiAccount(value);
}
get setInstance(): string {
return this.instance;
}
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly authService: AuthService,
private readonly store: Store) { }
@ -51,8 +54,27 @@ export class AddNewAccountComponent implements OnInit {
this.isComrade = false;
}
isInstanceMultiAccount: boolean;
isInstanceMultiAccountLoading: boolean;
checkInstanceMultiAccount(value: string) {
if(value) {
const instances: string[] = this.toolsService.getAllAccounts().map(x => x.instance);
if(instances && instances.indexOf(value) > -1){
this.isInstanceMultiAccount = true;
this.isInstanceMultiAccountLoading = true;
setTimeout(() => {
this.isInstanceMultiAccountLoading = false;
}, 2000);
} else {
this.isInstanceMultiAccount = false;
this.isInstanceMultiAccountLoading = false;
}
}
}
onSubmit(): boolean {
if(this.isLoading || !this.instance) return false;
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
this.isLoading = true;

View File

@ -3,7 +3,6 @@
<div class=" new-message-body flexcroll">
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
[replyingUserHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-create-status>
[replyingUserHandle]="userHandle" [statusToEdit]="statusToEdit" [redraftedStatus]="redraftedStatus"></app-create-status>
</div>
</div>

View File

@ -13,6 +13,7 @@ export class AddNewStatusComponent implements OnInit {
@Input() isDirectMention: boolean;
@Input() userHandle: string;
@Input() redraftedStatus: StatusWrapper;
@Input() statusToEdit: StatusWrapper;
constructor(private readonly navigationService: NavigationService) {
}

View File

@ -1,29 +1,31 @@
<div class="floating-column">
<div class="floating-column__inner">
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
(closeOverlay)="closeOverlay()"
[browseAccountData]="overlayAccountToBrowse"
[browseAccountData]="overlayAccountToBrowse"
[browseHashtagData]="overlayHashtagToBrowse"
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
<div class="floating-column__inner--left">
<div class="floating-column__header">
<a class="close-button" href (click)="closePanel()" title="close">
<fa-icon [icon]="faTimes"></fa-icon>
<fa-icon class="close-button__icon" [icon]="faTimes"></fa-icon>
</a>
</div>
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
(browseAccountEvent)="browseAccount($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-manage-account>
<app-add-new-status *ngIf="openPanel === 'createNewStatus'" [isDirectMention]="isDirectMention"
[userHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-add-new-status>
[userHandle]="userHandle"
[redraftedStatus]="redraftedStatus"
[statusToEdit]="statusToEdit"></app-add-new-status>
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
<app-search *ngIf="openPanel === 'search'"
<app-search *ngIf="openPanel === 'search'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)">
</app-search>
<app-settings *ngIf="openPanel === 'settings'"></app-settings>

View File

@ -29,9 +29,20 @@
}
.close-button {
// outline: 1px dotted orange;
display: block;
float: right;
font-size: 14px;
color: white;
margin: 10px 16px 0 0;
margin: 5px 5px 0 0;
width: 40px;
height: 34px;
&__icon {
position: relative;
top: 6px;
left: 17px;
}
}

View File

@ -25,6 +25,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
isDirectMention: boolean;
userHandle: string;
redraftedStatus: StatusWrapper;
statusToEdit: StatusWrapper;
openPanel: string = '';
@ -49,12 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
}
break;
case LeftPanelType.CreateNewStatus:
case LeftPanelType.EditStatus:
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
this.closePanel();
} else {
this.isDirectMention = event.action === LeftPanelAction.DM;
this.userHandle = event.userHandle;
this.redraftedStatus = event.status;
if(event.type === LeftPanelType.CreateNewStatus){
this.redraftedStatus = event.status;
this.statusToEdit = null;
} else {
this.redraftedStatus = null;
this.statusToEdit = event.status;
}
this.openPanel = 'createNewStatus';
}
break;

View File

@ -1,3 +0,0 @@
<p>
bookmarks works!
</p>

View File

@ -0,0 +1,5 @@
@import "variables";
.stream-toots {
background-color: $column-background;
}

View File

@ -1,141 +1,99 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { StatusWrapper } from '../../../../models/common.model';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountWrapper } from '../../../../models/account.models';
import { FavoriteResult, BookmarkResult } from '../../../../services/mastodon.service';
import { BookmarkResult } from '../../../../services/mastodon.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Status } from '../../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../../services/notification.service';
import { TimeLineModeEnum } from '../../../../states/settings.state';
import { TimelineBase } from '../../../../components/common/timeline-base';
@Component({
selector: 'app-bookmarks',
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './bookmarks.component.scss']
})
export class BookmarksComponent implements OnInit {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = true;
isThread = false;
hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private maxReached = false;
export class BookmarksComponent extends TimelineBase {
private maxId: string;
private _account: AccountWrapper;
private _accountWrapper: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
set accountWrapper(acc: AccountWrapper) {
this._accountWrapper = acc;
this.account = acc.info;
this.getBookmarks();
}
get account(): AccountWrapper {
return this._account;
get accountWrapper(): AccountWrapper {
return this._accountWrapper;
}
@ViewChild('statusstream') public statustream: ElementRef;
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
}
ngOnInit() {
}
ngOnDestroy() {
}
private reset() {
this.isLoading = true;
this.statuses.length = 0;
this.maxReached = false;
this.maxReached = false;
this.lastCallReachedMax = false;
this.maxId = null;
}
private getBookmarks() {
this.reset();
this.mastodonService.getBookmarks(this.account.info)
this.mastodonService.getBookmarks(this.account)
.then((result: BookmarkResult) => {
this.maxId = result.max_id;
if(!this.maxId){
this.lastCallReachedMax = true;
}
for (const s of result.bookmarked) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
});
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
protected getNextStatuses(): Promise<Status[]> {
if(this.lastCallReachedMax) return Promise.resolve([]);
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.isLoading || this.maxReached) return;
this.isLoading = true;
this.mastodonService.getBookmarks(this.account.info, this.maxId)
return this.mastodonService.getBookmarks(this.account, this.maxId)
.then((result: BookmarkResult) => {
const statuses = result.bookmarked;
if (statuses.length === 0 || !this.maxId) {
this.maxReached = true;
return;
}
this.maxId = result.max_id;
for (const s of statuses) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
if(!this.maxId){
this.lastCallReachedMax = true;
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
return statuses;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
protected scrolledToTop() {}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
protected statusProcessOnGoToTop(){}
}

View File

@ -8,13 +8,14 @@ import { NotificationService } from '../../../../services/notification.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Conversation } from '../../../../services/models/mastodon.interfaces';
import { AccountInfo } from '../../../../states/accounts.state';
import { BrowseBase } from '../../../common/browse-base';
@Component({
selector: 'app-direct-messages',
templateUrl: './direct-messages.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './direct-messages.component.scss']
})
export class DirectMessagesComponent implements OnInit {
export class DirectMessagesComponent extends BrowseBase {
faUserFriends = faUserFriends;
conversations: ConversationWrapper[] = [];
@ -23,9 +24,7 @@ export class DirectMessagesComponent implements OnInit {
isThread = false;
hasContentWarnings = false;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private isProcessingInfiniteScroll: boolean;
private maxReached = false;
private _account: AccountWrapper;
@ -44,11 +43,16 @@ export class DirectMessagesComponent implements OnInit {
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
private readonly mastodonService: MastodonWrapperService) {
super();
}
ngOnInit() {
}
ngOnDestroy() {
}
private reset() {
this.isLoading = true;
this.conversations.length = 0;
@ -78,17 +82,19 @@ export class DirectMessagesComponent implements OnInit {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
}
}
private scrolledErrorOccured = false;
private scrolledToBottom() {
if (this.isLoading || this.maxReached) return;
if (this.isLoading || this.maxReached || this.scrolledErrorOccured) return;
const maxId = this.conversations[this.conversations.length - 1].conversation.last_status.id;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
this.mastodonService.getConversations(this.account.info, maxId)
.then((conversations: Conversation[]) => {
if (conversations.length === 0) {
@ -103,25 +109,19 @@ export class DirectMessagesComponent implements OnInit {
}
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {

View File

@ -1,3 +0,0 @@
<p>
favorites works!
</p>

View File

@ -0,0 +1,5 @@
@import "variables";
.stream-toots {
background-color: $column-background;
}

View File

@ -1,78 +1,74 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core';
import { Component, Input } from '@angular/core';
import { StatusWrapper } from '../../../../models/common.model';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountWrapper } from '../../../../models/account.models';
import { FavoriteResult } from '../../../../services/mastodon.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Status } from '../../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../../services/notification.service';
import { TimeLineModeEnum } from '../../../../states/settings.state';
import { TimelineBase } from '../../../../components/common/timeline-base';
@Component({
selector: 'app-favorites',
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './favorites.component.scss']
})
export class FavoritesComponent implements OnInit {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = true;
isThread = false;
hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private maxReached = false;
export class FavoritesComponent extends TimelineBase {
private maxId: string;
private _account: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
set accountWrapper(acc: AccountWrapper) {
this._account = acc;
this.account = acc.info;
this.getFavorites();
}
get account(): AccountWrapper {
get accountWrapper(): AccountWrapper {
return this._account;
}
@ViewChild('statusstream') public statustream: ElementRef;
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
}
ngOnInit() {
}
private reset(){
ngOnDestroy() {
}
private reset() {
this.isLoading = true;
this.statuses.length = 0;
this.maxReached = false;
this.lastCallReachedMax = false;
this.maxId = null;
}
private getFavorites() {
this.reset();
this.mastodonService.getFavorites(this.account.info)
this.mastodonService.getFavorites(this.account)
.then((result: FavoriteResult) => {
this.maxId = result.max_id;
if (!this.maxId) {
this.lastCallReachedMax = true;
}
for (const s of result.favorites) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
@ -80,63 +76,23 @@ export class FavoritesComponent implements OnInit {
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
protected getNextStatuses(): Promise<Status[]> {
if (this.lastCallReachedMax) return Promise.resolve([]);
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.isLoading || this.maxReached) return;
this.isLoading = true;
this.mastodonService.getFavorites(this.account.info, this.maxId)
return this.mastodonService.getFavorites(this.account, this.maxId)
.then((result: FavoriteResult) => {
const statuses = result.favorites;
if (statuses.length === 0 || !this.maxId) {
this.maxReached = true;
return;
}
this.maxId = result.max_id;
for (const s of statuses) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
if (!this.maxId) {
this.lastCallReachedMax = true;
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
return statuses;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
protected scrolledToTop() { }
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
protected statusProcessOnGoToTop() { }
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons";
import { faBell, faEnvelope, faUser, faStar, faBookmark } from "@fortawesome/free-regular-svg-icons";
import { Subscription } from 'rxjs';
@ -15,6 +15,8 @@ import { NotificationsComponent } from './notifications/notifications.component'
import { MentionsComponent } from './mentions/mentions.component';
import { DirectMessagesComponent } from './direct-messages/direct-messages.component';
import { FavoritesComponent } from './favorites/favorites.component';
import { BrowseBase } from '../../common/browse-base';
import { SettingsService } from '../../../services/settings.service';
@Component({
@ -22,7 +24,7 @@ import { FavoritesComponent } from './favorites/favorites.component';
templateUrl: './manage-account.component.html',
styleUrls: ['./manage-account.component.scss']
})
export class ManageAccountComponent implements OnInit, OnDestroy {
export class ManageAccountComponent extends BrowseBase {
faAt = faAt;
faBell = faBell;
faEnvelope = faEnvelope;
@ -38,10 +40,6 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
userAccount: Account;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
@ -57,12 +55,15 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
private _account: AccountWrapper;
constructor(
private readonly settingsService: SettingsService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService) { }
private readonly userNotificationService: UserNotificationService) {
super();
}
ngOnInit() {
ngOnInit() {
}
ngOnDestroy(): void {
@ -70,13 +71,9 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
}
private checkIfBookmarksAreAvailable() {
this.toolsService.getInstanceInfo(this.account.info)
.then((instance: InstanceInfo) => {
if (instance.major >= 3 && instance.minor >= 1) {
this.isBookmarksAvailable = true;
} else {
this.isBookmarksAvailable = false;
}
this.toolsService.isBookmarksAreAvailable(this.account.info)
.then((isAvailable: boolean) => {
this.isBookmarksAvailable = isAvailable;
})
.catch(err => {
this.isBookmarksAvailable = false;
@ -101,8 +98,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
if (userNotification) {
let settings = this.toolsService.getSettings();
let accSettings = this.toolsService.getAccountSettings(this.account.info);
let settings = this.settingsService.getSettings();
let accSettings = this.settingsService.getAccountSettings(this.account.info);
if (!settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
this.hasNotifications = userNotification.hasNewNotifications;
@ -114,8 +111,8 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
let current = this.userNotificationService.userNotifications.value;
const userNotification = current.find(x => x.account.id === this.account.info.id);
if (userNotification) {
let settings = this.toolsService.getSettings();
let accSettings = this.toolsService.getAccountSettings(this.account.info);
let settings = this.settingsService.getSettings();
let accSettings = this.settingsService.getAccountSettings(this.account.info);
if (!settings.disableAutofocus && !settings.disableAvatarNotifications && !accSettings.disableAvatarNotifications) {
if (userNotification.hasNewNotifications) {
@ -127,16 +124,16 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
}
}
@ViewChild('bookmarks') bookmarksComp:BookmarksComponent;
@ViewChild('notifications') notificationsComp:NotificationsComponent;
@ViewChild('mentions') mentionsComp:MentionsComponent;
@ViewChild('dm') dmComp:DirectMessagesComponent;
@ViewChild('favorites') favoritesComp:FavoritesComponent;
@ViewChild('bookmarks') bookmarksComp: BookmarksComponent;
@ViewChild('notifications') notificationsComp: NotificationsComponent;
@ViewChild('mentions') mentionsComp: MentionsComponent;
@ViewChild('dm') dmComp: DirectMessagesComponent;
@ViewChild('favorites') favoritesComp: FavoritesComponent;
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
if(this.subPanel === subpanel){
switch(subpanel){
case 'bookmarks':
if (this.subPanel === subpanel) {
switch (subpanel) {
case 'bookmarks':
this.bookmarksComp.applyGoToTop();
break;
case 'notifications':
@ -150,17 +147,13 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
break;
case 'favorites':
this.favoritesComp.applyGoToTop();
break;
break;
}
}
this.subPanel = subpanel;
return false;
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
return false;
}
browseLocalAccount(): boolean {
@ -173,12 +166,4 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
window.open(this.userAccount.url, '_blank');
return false;
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -1,3 +0,0 @@
<p>
mentions works!
</p>

View File

@ -1,6 +1,4 @@
@import "variables";
@import "commons";
@import "mixins";
.stream-toots {
background-color: $column-background;

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Subscription } from 'rxjs';
import { AccountWrapper } from '../../../../models/account.models';
@ -7,8 +7,8 @@ import { StatusWrapper } from '../../../../models/common.model';
import { Status, Notification } from '../../../../services/models/mastodon.interfaces';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../../services/notification.service';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { TimeLineModeEnum } from '../../../../states/settings.state';
import { ToolsService } from '../../../../services/tools.service';
import { TimelineBase } from '../../../../components/common/timeline-base';
@Component({
@ -16,46 +16,33 @@ import { TimeLineModeEnum } from '../../../../states/settings.state';
templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html',
styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './mentions.component.scss']
})
export class MentionsComponent implements OnInit, OnDestroy {
statuses: StatusWrapper[] = [];
displayError: string;
isLoading = false;
isThread = false;
hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
export class MentionsComponent extends TimelineBase {
private lastId: string;
private _account: AccountWrapper;
@Input('account')
set account(acc: AccountWrapper) {
set accountWrapper(acc: AccountWrapper) {
this._account = acc;
this.account = acc.info;
this.loadMentions();
}
get account(): AccountWrapper {
get accountWrapper(): AccountWrapper {
return this._account;
}
@ViewChild('statusstream') public statustream: ElementRef;
private maxReached = false;
private _account: AccountWrapper;
private userNotificationServiceSub: Subscription;
private lastId: string;
private userNotificationServiceSub: Subscription;
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly mastodonService: MastodonWrapperService) {
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly userNotificationService: UserNotificationService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
}
ngOnInit() {
this.isLoading = false;
}
ngOnDestroy(): void {
@ -70,7 +57,7 @@ export class MentionsComponent implements OnInit, OnDestroy {
}
this.statuses.length = 0;
this.userNotificationService.markMentionsAsRead(this.account.info);
this.userNotificationService.markMentionsAsRead(this.account);
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
this.processNewMentions(userNotifications);
@ -79,82 +66,33 @@ export class MentionsComponent implements OnInit, OnDestroy {
}
private processNewMentions(userNotifications: UserNotification[]) {
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
const userNotification = userNotifications.find(x => x.account.id === this.account.id);
if (userNotification && userNotification.mentions) {
let orderedMentions = [...userNotification.mentions.map(x => x.status)].reverse();
for (let m of orderedMentions) {
if (!this.statuses.find(x => x.status.id === m.id)) {
if (m && !this.statuses.find(x => x.status.id === m.id)) {
let cwPolicy = this.toolsService.checkContentWarning(m);
const statusWrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
const statusWrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.unshift(statusWrapper);
}
}
}
this.lastId = userNotification.lastMentionsId;
this.userNotificationService.markMentionsAsRead(this.account.info);
this.userNotificationService.markMentionsAsRead(this.account);
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
this.scrolledToBottom();
}
}
private scrolledToBottom() {
if (this.isLoading || this.maxReached || this.statuses.length === 0) return;
this.isLoading = true;
this.mastodonService.getNotifications(this.account.info, ['follow', 'favourite', 'reblog', 'poll'], this.lastId)
.then((result: Notification[]) => {
protected getNextStatuses(): Promise<Status[]> {
return this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll', 'move', 'update'], this.lastId)
.then((result: Notification[]) => {
const statuses = result.map(x => x.status);
if (statuses.length === 0) {
this.maxReached = true;
return;
}
this.lastId = result[result.length - 1].id;
return statuses;
});
}
protected scrolledToTop() {}
for (const s of statuses) {
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
if (!this.statuses.find(x => x.status.id === s.id)) {
this.statuses.push(wrapper);
}
}
this.lastId = result[result.length - 1].id;
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
protected statusProcessOnGoToTop(){}
}

View File

@ -6,7 +6,7 @@ import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper
import { AccountWrapper } from '../../../../../models/account.models';
import { NotificationService } from '../../../../../services/notification.service';
import { Account, Relationship, Instance } from "../../../../../services/models/mastodon.interfaces";
import { of } from 'rxjs';
import { SettingsService } from '../../../../../services/settings.service';
@Component({
selector: 'app-list-editor',
@ -25,6 +25,7 @@ export class ListEditorComponent implements OnInit {
searchOpen: boolean;
constructor(
private readonly settingsService: SettingsService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonWrapperService) { }
@ -69,13 +70,12 @@ export class ListEditorComponent implements OnInit {
}
addEvent(accountWrapper: AccountListWrapper) {
console.log(accountWrapper);
const settings = this.settingsService.getSettings();
accountWrapper.isLoading = true;
this.mastodonService.getInstance(this.account.info.instance)
.then((instance: Instance) => {
console.log(instance);
if (instance.version.toLowerCase().includes('pleroma')) {
if (instance.version.toLowerCase().includes('pleroma') && !settings.autoFollowOnListEnabled) {
return Promise.resolve(true);
} else {
return this.followAccount(accountWrapper);

View File

@ -65,6 +65,6 @@
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4>
<a class="my-account__link my-account__red" href (click)="removeAccount()">
Delete
Remove
</a>
</div>

View File

@ -10,8 +10,8 @@ import { AccountWrapper } from '../../../../models/account.models';
import { RemoveAccount } from '../../../../states/accounts.state';
import { NavigationService } from '../../../../services/navigation.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { ToolsService } from '../../../../services/tools.service';
import { AccountSettings } from '../../../../states/settings.state';
import { SettingsService } from '../../../../services/settings.service';
@Component({
selector: 'app-my-account',
@ -49,8 +49,8 @@ export class MyAccountComponent implements OnInit, OnDestroy {
private streamChangedSub: Subscription;
constructor(
private readonly settingsService: SettingsService,
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonWrapperService,
private readonly notificationService: NotificationService) { }
@ -68,7 +68,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
}
private loadAccountSettings(){
this.accountSettings = this.toolsService.getAccountSettings(this.account.info);
this.accountSettings = this.settingsService.getAccountSettings(this.account.info);
this.customStatusLengthEnabled = this.accountSettings.customStatusCharLengthEnabled;
this.customStatusLength = this.accountSettings.customStatusCharLength;
@ -77,13 +77,13 @@ export class MyAccountComponent implements OnInit, OnDestroy {
onCustomLengthEnabledChanged(): boolean {
this.accountSettings.customStatusCharLengthEnabled = this.customStatusLengthEnabled;
this.toolsService.saveAccountSettings(this.accountSettings);
this.settingsService.saveAccountSettings(this.accountSettings);
return false;
}
customStatusLengthChanged(event): boolean{
this.accountSettings.customStatusCharLength = this.customStatusLength;
this.toolsService.saveAccountSettings(this.accountSettings);
this.settingsService.saveAccountSettings(this.accountSettings);
return false;
}
@ -122,6 +122,17 @@ export class MyAccountComponent implements OnInit, OnDestroy {
}
}
})
.then(_ => {
this.availableLists.sort((a,b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
});
@ -203,9 +214,9 @@ export class MyAccountComponent implements OnInit, OnDestroy {
}
onDisableAvatarNotificationChanged() {
let settings = this.toolsService.getAccountSettings(this.account.info);
let settings = this.settingsService.getAccountSettings(this.account.info);
settings.disableAvatarNotifications = this.avatarNotificationDisabled;
this.toolsService.saveAccountSettings(settings);
this.settingsService.saveAccountSettings(settings);
}
}

View File

@ -1,4 +1,35 @@
<div class="notification">
<div *ngIf="notification.type === 'follow_request' && !followRequestProcessed">
<div class="stream__notification--icon" title="{{notification.account.acct}}">
<fa-icon class="followed" [icon]="faUserClock"></fa-icon>
</div>
<div class="stream__notification--label">
<a href class="stream__link" title="{{notification.account.acct}}"
(click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
innerHTML="{{ notification.account | accountEmoji }}"></a>
submitted a follow request
</div>
<a href (click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
class="follow-account" title="{{notification.account.acct}}">
<img class="follow-account__avatar" src="{{ notification.account.avatar }}" />
<span class="follow-account__display-name" innerHTML="{{ notification.account | accountEmoji }}"></span>
<span class="follow-account__acct">@{{ notification.account.acct }}</span>
</a>
<div class="follow_request">
<a href title="Authorize" class="follow_request__link follow_request__link--check"
(click)="acceptFollowRequest()">
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
</a>
<a href title="Reject" class="follow_request__link follow_request__link--cross"
(click)="refuseFollowRequest()">
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
</a>
</div>
</div>
<div *ngIf="notification.type === 'follow'">
<div class="stream__notification--icon" title="{{notification.account.acct}}">
<fa-icon class="followed" [icon]="faUserPlus"></fa-icon>
@ -19,12 +50,49 @@
</a>
</div>
<app-status *ngIf="notification.status && notification.type !== 'mention'" class="stream__status" [statusWrapper]="notification.status"
[notificationAccount]="notification.account" [notificationType]="notification.type"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
<div *ngIf="notification.type === 'move'">
<div class="stream__notification--icon" title="{{notification.account.acct}}">
<fa-icon class="followed" [icon]="faTruckMoving"></fa-icon>
</div>
<div class="stream__notification--label">
<a href class="stream__link" title="{{notification.account.acct}}"
(click)="openAccount(notification.account)" (auxclick)="openUrl(notification.account.url)"
innerHTML="{{ notification.account | accountEmoji }}"></a>
migrated to
</div>
<a href (click)="openAccount(notification.target)" (auxclick)="openUrl(notification.target.url)"
class="follow-account" title="{{notification.target.acct}}">
<img class="follow-account__avatar" src="{{ notification.target.avatar }}" />
<span class="follow-account__display-name" innerHTML="{{ notification.target | accountEmoji }}"></span>
<span class="follow-account__acct">@{{ notification.target.acct }}</span>
</a>
</div>
<app-status *ngIf="notification.status && notification.type === 'update'" class="stream__status"
[statusWrapper]="notification.status"
[notificationAccount]="notification.account"
[notificationType]="notification.type"
[context]="'notifications'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status" [statusWrapper]="notification.status"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status"
[statusWrapper]="notification.status"
[context]="'notifications'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
<app-status *ngIf="notification.status && notification.type !== 'mention' && notification.type !== 'update'"
class="stream__status"
[statusWrapper]="notification.status"
[notificationAccount]="notification.account"
[notificationType]="notification.type"
[context]="'notifications'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div>

View File

@ -46,6 +46,7 @@
color: $boost-color;
}
$acccount-info-left: 70px;
.follow-account {
padding: 5px;
height: 60px;
@ -62,8 +63,7 @@
height: 45px;
border-radius: 2px;
}
$acccount-info-left: 70px;
&__display-name {
position: absolute;
top: 7px;
@ -81,5 +81,44 @@
left: $acccount-info-left;
font-size: 13px;
color: $status-links-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: calc(100% - #{$acccount-info-left});
}
}
.follow_request {
width: calc(100% - #{$acccount-info-left});
margin-left: $acccount-info-left;
&__link {
display: inline-block;
width: calc(50%);
padding: 2px;
text-align: center;
color: rgb(182, 182, 182);
transition: all .2s;
// outline: 1px dotted greenyellow;
&--check {
&:hover {
color: greenyellow;
}
}
&--cross {
&:hover {
color: orangered;
}
}
}
&__icon {
text-align: center;
}
}

View File

@ -1,39 +1,38 @@
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { faUserPlus } from "@fortawesome/free-solid-svg-icons";
import { Component, Input } from '@angular/core';
import { faUserPlus, faUserClock, faCheck, faTimes, faTruckMoving } from "@fortawesome/free-solid-svg-icons";
import { NotificationWrapper } from '../notifications.component';
import { OpenThreadEvent, ToolsService } from '../../../../../services/tools.service';
import { ToolsService } from '../../../../../services/tools.service';
import { Account } from '../../../../../services/models/mastodon.interfaces';
import { BrowseBase } from '../../../../../components/common/browse-base';
import { MastodonWrapperService } from '../../../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../../../services/notification.service';
@Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.scss']
})
export class NotificationComponent implements OnInit {
export class NotificationComponent extends BrowseBase {
faUserPlus = faUserPlus;
faUserClock = faUserClock;
faCheck = faCheck;
faTimes = faTimes;
faTruckMoving = faTruckMoving;
@Input() notification: NotificationWrapper;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
constructor(private readonly toolsService: ToolsService) { }
constructor(
private readonly notificationsService: NotificationService,
private readonly mastodonService: MastodonWrapperService,
private readonly toolsService: ToolsService) {
super();
}
ngOnInit() {
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
ngOnDestroy() {
}
openAccount(account: Account): boolean {
@ -41,9 +40,47 @@ export class NotificationComponent implements OnInit {
this.browseAccountEvent.next(accountName);
return false;
}
openUrl(url: string): boolean {
window.open(url, '_blank');
return false;
}
followRequestWorking: boolean;
followRequestProcessed: boolean;
acceptFollowRequest(): boolean {
if(this.followRequestWorking) return false;
this.followRequestWorking = true;
this.mastodonService.authorizeFollowRequest(this.notification.provider, this.notification.notification.account.id)
.then(res => {
this.followRequestProcessed = true;
})
.catch(err => {
this.notificationsService.notifyHttpError(err, this.notification.provider);
})
.then(res => {
this.followRequestWorking = false;
});
return false;
}
refuseFollowRequest(): boolean {
if(this.followRequestWorking) return false;
this.followRequestWorking = true;
this.mastodonService.rejectFollowRequest(this.notification.provider, this.notification.notification.account.id)
.then(res => {
this.followRequestProcessed = true;
})
.catch(err => {
this.notificationsService.notifyHttpError(err, this.notification.provider);
})
.then(res => {
this.followRequestWorking = false;
});
return false;
}
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core';
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { AccountWrapper } from '../../../../models/account.models';
@ -8,21 +8,19 @@ import { Notification, Account } from '../../../../services/models/mastodon.inte
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../../services/notification.service';
import { AccountInfo } from '../../../../states/accounts.state';
import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service';
import { ToolsService } from '../../../../services/tools.service';
import { BrowseBase } from '../../../../components/common/browse-base';
@Component({
selector: 'app-notifications',
templateUrl: './notifications.component.html',
styleUrls: ['./notifications.component.scss']
})
export class NotificationsComponent implements OnInit, OnDestroy {
export class NotificationsComponent extends BrowseBase {
notifications: NotificationWrapper[] = [];
private isProcessingInfiniteScroll: boolean;
isLoading = false;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('account')
set account(acc: AccountWrapper) {
this._account = acc;
@ -43,7 +41,9 @@ export class NotificationsComponent implements OnInit, OnDestroy {
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly mastodonService: MastodonWrapperService) { }
private readonly mastodonService: MastodonWrapperService) {
super();
}
ngOnInit() {
}
@ -89,15 +89,17 @@ export class NotificationsComponent implements OnInit, OnDestroy {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
if (atBottom) {
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
}
}
private scrolledErrorOccured = false;
private scrolledToBottom() {
if (this.isLoading || this.maxReached || this.notifications.length === 0) return;
if (this.isLoading || this.maxReached || this.notifications.length === 0 || this.scrolledErrorOccured) return;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
this.mastodonService.getNotifications(this.account.info, ['mention'], this.lastId)
.then((notifications: Notification[]) => {
@ -117,25 +119,19 @@ export class NotificationsComponent implements OnInit, OnDestroy {
this.lastId = notifications[notifications.length - 1].id;
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
});
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
applyGoToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
@ -156,17 +152,22 @@ export class NotificationWrapper {
case 'reblog':
case 'favourite':
case 'poll':
case 'update':
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
break;
}
this.account = notification.account;
this.target = notification.target;
this.wrapperId = `${this.type}-${notification.id}`;
this.notification = notification;
this.provider = provider;
}
provider: AccountInfo;
notification: Notification;
wrapperId: string;
account: Account;
target: Account;
status: StatusWrapper;
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll';
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request' | 'move' | 'update';
}

View File

@ -4,8 +4,8 @@
<h3 class="panel__title">search</h3>
<form class="form-section" (ngSubmit)="onSubmit()">
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
name="searchHandle" placeholder="Search" autocomplete="off" />
<input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
<button class="form-button" type="submit" title="search">GO</button>
</form>
</div>
@ -15,12 +15,17 @@
<div *ngIf="accounts.length > 0" class="search-results">
<h3 class="search-results__title">Accounts</h3>
<a href *ngFor="let account of accounts" class="account" title="open account"
<app-account class="account" *ngFor="let account of accounts"
[account]="account"
(accountSelected)="processAndBrowseAccount($event)"></app-account>
<!-- <a href *ngFor="let account of accounts" class="account" title="open account"
(click)="processAndBrowseAccount(account)">
<img src="{{account.avatar}}" class="account__avatar" />
<div class="account__name">{{ account.username }}</div>
<div class="account__fullhandle">@{{ account.acct }}</div>
</a>
</a> -->
</div>
<div *ngIf="hashtags.length > 0" class="search-results">

View File

@ -96,7 +96,7 @@ $search-form-height: 70px;
// outline: 1px solid greenyellow;
margin-top: 10px; // &:first-of-type{
padding-left: 10px; // margin-top: 10px;
padding-right: 10px; // margin-top: 10px;
//padding-right: 10px; // margin-top: 10px;
// }
&__title {
@ -135,42 +135,4 @@ $search-form-height: 70px;
.account {
display: block;
color: white;
border-radius: 2px;
transition: all .3s; // &:hover &__name {
// text-decoration: underline;
// }
border-top: 1px solid $separator-color;
&:last-of-type {
border-bottom: 1px solid $separator-color;
}
&__avatar {
width: 40px;
margin: 5px 10px 5px 5px;
float: left;
border-radius: 2px;
}
&__name {
margin: 7px 0 0 0;
}
&__fullhandle {
margin: 0 0 5px 0;
color: $status-secondary-color;
transition: all .3s; // &:hover {
// color: white;
// }
}
&:hover,
&:hover &__fullhandle {
color: white;
text-decoration: none;
background-color: $button-background-color-hover;
}
@include clearfix;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ViewChild('search') searchElement: ElementRef;
constructor(
private readonly notificationService: NotificationService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService) { }
ngOnInit() {
this.searchElement.nativeElement.focus();
}
onSubmit(): boolean {

View File

@ -35,6 +35,7 @@
</form>
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
</div>
<h4 class="panel__subtitle">Shortcuts</h4>
<div class="sub-section">
<span class="sub-section__title">switch column:</span><br />
@ -50,6 +51,53 @@
<br>
</div>
<h4 class="panel__subtitle">Languages</h4>
<div class="sub-section">
<div class="sub-section__content">
<div *ngIf="!configuredLangs || configuredLangs.length === 0" class="language__warning">
No language set.
</div>
<div *ngFor="let l of configuredLangs" class="language__entry">
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
<a href (click)="onRemoveLang(l)" class="form-button language__entry__action sound__play">remove</a>
</div>
<input type="text" (input)="onSearchLang($event.target.value)" [(ngModel)]="searchLang"
placeholder="Find Language" autocomplete="off"
class="form-control form-control-sm language__search" />
<div *ngFor="let l of searchedLangs" class="language__entry">
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
<a href (click)="onAddLang(l)" class="form-button language__entry__action sound__play">add</a>
</div>
</div>
<input class="sub-section__checkbox" [(ngModel)]="disableLangAutodetectEnabled"
(change)="onDisableLangAutodetectChanged()" type="checkbox" name="disableLangAutodetec"
value="disableLangAutodetec" id="disableLangAutodetec">
<label class="noselect sub-section__label" for="disableLangAutodetec">disable language autodetection</label>
</div>
<h4 class="panel__subtitle">Twitter Bridge</h4>
<div class="sub-section">
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
<br>
<div *ngIf="twitterBridgeEnabled">
<p>Please provide your bridge instance:
<input type="text" class="form-control form-control-sm sub_section__text-input"
[(ngModel)]="setTwitterBridgeInstance" placeholder="bridge.tld" />
If you don't know any, consider using <a href="https://github.com/NicolasConstant/BirdsiteLive"
target="_blank" class="version__link">BirdsiteLIVE</a>
</p>
</div>
<div>
<a href="https://github.com/NicolasConstant/sengi/wiki/BirdsiteLIVE-integration" target="_blank"
class="version__link">What is this?</a>
</div>
</div>
<h4 class="panel__subtitle">Content-Warning Policies</h4>
<div class="sub-section">
<span class="sub-section__title">global behavior:</span><br />
@ -60,7 +108,7 @@
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 2" (change)="onCwPolicyChange(2)"
type="radio" name="cw-hide-all" value="cw-hide-all" id="cw-hide-all">
<label class="noselect sub-section__label" for="cw-hide-all">Hide all CWs</label>
<label class="noselect sub-section__label" for="cw-hide-all">Expand all CWs</label>
<br>
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
<span class="sub-section__title">but add CW on content containing:</span><br />
@ -117,6 +165,12 @@
<label class="noselect sub-section__label" for="timelineheader-5">Title</label>
<br>
<input class="sub-section__checkbox" [checked]="timeLineHeader === 6" (change)="onTimeLineHeaderChange(6)"
type="radio" name="timelineheader-6" value="timelineheader-6" id="timelineheader-6">
<label class="noselect sub-section__label" for="timelineheader-6">Title - Account Icon - Username - Domain
Name</label>
<br>
<span class="sub-section__title">loading behavior:</span><br />
<input class="sub-section__checkbox" [checked]="timeLineMode === 1" (change)="onTimeLineModeChange(1)"
@ -135,6 +189,18 @@
<br>
</div>
<div *ngIf="hasPleromaAccount">
<h4 class="panel__subtitle">Pleroma</h4>
<div class="sub-section">
<input class="sub-section__checkbox" [(ngModel)]="autoFollowOnListEnabled"
(change)="onAutoFollowOnListChanged()" type="checkbox" name="onAutoFollowOnListChanged"
value="onAutoFollowOnListChanged" id="onAutoFollowOnListChanged">
<label class="noselect sub-section__label" for="onAutoFollowOnListChanged">autofollow accounts when
adding to list</label>
<br>
</div>
</div>
<h4 class="panel__subtitle">Other</h4>
<div class="sub-section">
<input class="sub-section__checkbox" [(ngModel)]="disableRemoteStatusFetchingEnabled"
@ -143,11 +209,27 @@
<label class="noselect sub-section__label" for="disableRemoteFetching">disable remote status
fetching</label>
<br>
<input class="sub-section__checkbox" [(ngModel)]="enableAltLabelEnabled"
(change)="onEnableAltLabelChanged()" type="checkbox" name="enableAltLabel"
value="enableAltLabel" id="enableAltLabel">
<label class="noselect sub-section__label" for="enableAltLabel">enable alt label</label>
<br>
<input class="sub-section__checkbox" [(ngModel)]="enableFreezeAvatarEnabled"
(change)="onEnableFreezeAvatarChanged()" type="checkbox" name="enableFreezeAvatar"
value="enableFreezeAvatar" id="enableFreezeAvatar">
<label class="noselect sub-section__label" for="enableFreezeAvatar">freeze animated avatar</label>
<br>
reorder account's icons: <a href class="toogle-lock-icon-menu" (click)="toogleLockIconMenu()"><span *ngIf="iconMenuLocked">Unlock Icons</span><span *ngIf="!iconMenuLocked">Lock Icons</span></a>
</div>
<h4 class="panel__subtitle">About</h4>
<p class="version">
Sengi version: {{version}}<br />
<a href class="version__link" (click)="openTutorial()">open tutorial</a><br />
<a href="assets/docs/privacy.html" class="version__link" target="_blank">imprint & privacy</a><br />
<a href class="version__link" (click)="checkForUpdates()">check for updates</a>
<app-waiting-animation *ngIf="isCheckingUpdates" class="waiting-icon"></app-waiting-animation>
</p>

View File

@ -31,6 +31,13 @@
padding: 0 5px 15px 5px;
position: relative;
&__content {
display: block;
padding: 0 0 0 5px;
// outline: 1px dotted greenyellow;
}
&__checkbox {
position: relative;
top: 3px;
@ -68,6 +75,41 @@
}
}
.language {
&__warning {
color: orange;
}
&__entry {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
&:not(:last-child){
margin-bottom: 1px;
}
&__name {
display: block;
align-items: stretch;
padding-left: 5px;
}
&__action {
align-items: stretch;
min-width: 70px;
text-align: center;
padding: 0 10px;
}
}
&__search {
display: block;
margin: 5px 0 5px 0;
}
}
.form-control {
border: 1px solid $settings-text-input-border;
color: $settings-text-input-foreground;
@ -111,4 +153,22 @@
background-color: #32384d;
}
}
}
.toogle-lock-icon-menu {
display: block;
padding: 3px 40px;
width: 170px;
float: right;
text-align: center;
color: white;
background-color: #1f2330;
&:hover {
text-decoration: none;
background-color: #32384d;
}
}

View File

@ -1,13 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Howl } from 'howler';
import { Subscription } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { ToolsService } from '../../../services/tools.service';
import { ToolsService, InstanceType } from '../../../services/tools.service';
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
import { ServiceWorkerService } from '../../../services/service-worker.service';
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../../../states/settings.state';
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum, ILanguage } from '../../../states/settings.state';
import { NotificationService } from '../../../services/notification.service';
import { NavigationService } from '../../../services/navigation.service';
import { SettingsService } from '../../../services/settings.service';
import { LanguageService } from '../../../services/language.service';
@Component({
selector: 'app-settings',
@ -15,7 +19,7 @@ import { NotificationService } from '../../../services/notification.service';
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
export class SettingsComponent implements OnInit, OnDestroy {
notificationSounds: NotificationSoundDefinition[];
notificationSoundId: string;
@ -25,13 +29,25 @@ export class SettingsComponent implements OnInit {
disableRemoteStatusFetchingEnabled: boolean;
disableAvatarNotificationsEnabled: boolean;
disableSoundsEnabled: boolean;
disableLangAutodetectEnabled: boolean;
enableAltLabelEnabled: boolean;
enableFreezeAvatarEnabled: boolean;
version: string;
hasPleromaAccount: boolean;
autoFollowOnListEnabled: boolean;
twitterBridgeEnabled: boolean;
columnShortcutEnabled: ColumnShortcut = ColumnShortcut.Ctrl;
timeLineHeader: TimeLineHeaderEnum = TimeLineHeaderEnum.Title_DomainName;
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
configuredLangs: ILanguage[] = [];
searchedLangs: ILanguage[] = [];
searchLang: string;
private addCwOnContent: string;
set setAddCwOnContent(value: string) {
this.setCwPolicy(null, value, null, null);
@ -59,17 +75,38 @@ export class SettingsComponent implements OnInit {
return this.contentHidedCompletely;
}
private twitterBridgeInstance: string;
set setTwitterBridgeInstance(value: string) {
let instance = value.replace('https://', '').replace('http://', '').replace('/', '').trim();
this.setBridgeInstance(instance);
this.twitterBridgeInstance = instance;
}
get setTwitterBridgeInstance(): string {
return this.twitterBridgeInstance;
}
private languageSub: Subscription;
constructor(
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService,
private readonly navigationService: NavigationService,
private formBuilder: FormBuilder,
private serviceWorkersService: ServiceWorkerService,
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly userNotificationsService: UserNotificationService) { }
private readonly userNotificationsService: UserNotificationService) { }
ngOnInit() {
this.languageSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
if(l){
this.configuredLangs = l;
}
});
this.version = environment.VERSION;
const settings = this.toolsService.getSettings();
const settings = this.settingsService.getSettings();
this.notificationSounds = this.userNotificationsService.getAllNotificationSounds();
this.notificationSoundId = settings.notificationSoundFileId;
@ -95,33 +132,86 @@ export class SettingsComponent implements OnInit {
this.timeLineHeader = settings.timelineHeader;
this.timeLineMode = settings.timelineMode;
this.autoFollowOnListEnabled = settings.autoFollowOnListEnabled;
const accs = this.toolsService.getAllAccounts();
accs.forEach(a => {
this.toolsService.getInstanceInfo(a)
.then(ins => {
if(ins.type === InstanceType.Pleroma){
this.hasPleromaAccount = true;
}
})
.catch(err => console.error(err));
});
this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
this.twitterBridgeInstance = settings.twitterBridgeInstance;
this.configuredLangs = this.languageService.getConfiguredLanguages();
this.disableLangAutodetectEnabled = settings.disableLangAutodetec;
this.enableAltLabelEnabled = settings.enableAltLabel;
this.enableFreezeAvatarEnabled = settings.enableFreezeAvatar;
}
ngOnDestroy(): void {
if(this.languageSub) this.languageSub.unsubscribe();
}
iconMenuLocked = true;
toogleLockIconMenu(): boolean {
this.navigationService.changeIconMenuState(this.iconMenuLocked);
this.iconMenuLocked = ! this.iconMenuLocked;
return false;
}
onSearchLang(input: string) {
this.searchedLangs = this.languageService.searchLanguage(input);
}
onAddLang(lang: ILanguage): boolean {
if(this.configuredLangs.findIndex(x => x.iso639 === lang.iso639) >= 0) return false;
// this.configuredLangs.push(lang);
this.languageService.addLanguage(lang);
this.searchLang = '';
this.searchedLangs.length = 0;
return false;
}
onRemoveLang(lang: ILanguage): boolean {
// this.configuredLangs = this.configuredLangs.filter(x => x.iso639 !== lang.iso639);
this.languageService.removeLanguage(lang);
return false;
}
onShortcutChange(id: ColumnShortcut) {
this.columnShortcutEnabled = id;
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.columnSwitchingWinAlt = id === ColumnShortcut.Win;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onTimeLineHeaderChange(id: TimeLineHeaderEnum){
this.timeLineHeader = id;
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.timelineHeader = id;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onTimeLineModeChange(id: TimeLineModeEnum){
this.timeLineMode = id;
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.timelineMode = id;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onCwPolicyChange(id: ContentWarningPolicyEnum) {
@ -133,7 +223,7 @@ export class SettingsComponent implements OnInit {
private setCwPolicy(id: ContentWarningPolicyEnum = null, addCw: string = null, removeCw: string = null, hide: string = null){
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
let cwPolicySettings = new ContentWarningPolicy();
if(id !== null){
@ -160,13 +250,19 @@ export class SettingsComponent implements OnInit {
cwPolicySettings.hideCompletlyContent = settings.contentWarningPolicy.hideCompletlyContent;
}
this.toolsService.saveContentWarningPolicy(cwPolicySettings);
}
this.settingsService.saveContentWarningPolicy(cwPolicySettings);
}
private splitCwValues(data: string): string[]{
return data.split(';').map(x => x.trim().toLowerCase()).filter((value, index, self) => self.indexOf(value) === index).filter(y => y !== '');
}
private setBridgeInstance(instance: string){
let settings = this.settingsService.getSettings();
settings.twitterBridgeInstance = instance;
this.settingsService.saveSettings(settings);
}
// reload(): boolean {
// window.location.reload();
// return false;
@ -174,9 +270,9 @@ export class SettingsComponent implements OnInit {
onChange(soundId: string) {
this.notificationSoundId = soundId;
let settings = this.toolsService.getSettings()
let settings = this.settingsService.getSettings()
settings.notificationSoundFileId = soundId;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
playNotificationSound(): boolean {
@ -190,31 +286,64 @@ export class SettingsComponent implements OnInit {
return false;
}
onEnableFreezeAvatarChanged(){
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();
settings.enableFreezeAvatar = this.enableFreezeAvatarEnabled;
this.settingsService.saveSettings(settings);
}
onEnableAltLabelChanged(){
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();
settings.enableAltLabel = this.enableAltLabelEnabled;
this.settingsService.saveSettings(settings);
}
onDisableLangAutodetectChanged() {
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();
settings.disableLangAutodetec = this.disableLangAutodetectEnabled;
this.settingsService.saveSettings(settings);
}
onDisableAutofocusChanged() {
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.disableAutofocus = this.disableAutofocusEnabled;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onDisableRemoteStatusFetchingChanged() {
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.disableRemoteStatusFetching = this.disableRemoteStatusFetchingEnabled;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onDisableAvatarNotificationsChanged() {
this.notifyRestartNeeded();
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.disableAvatarNotifications = this.disableAvatarNotificationsEnabled;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onDisableSoundsEnabledChanged() {
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
settings.disableSounds = this.disableSoundsEnabled;
this.toolsService.saveSettings(settings);
this.settingsService.saveSettings(settings);
}
onAutoFollowOnListChanged(){
let settings = this.settingsService.getSettings();
settings.autoFollowOnListEnabled = this.autoFollowOnListEnabled;
this.settingsService.saveSettings(settings);
}
onTwitterBridgeEnabledChanged(){
let settings = this.settingsService.getSettings();
settings.twitterBridgeEnabled = this.twitterBridgeEnabled;
this.settingsService.saveSettings(settings);
}
isCleanningAll: boolean = false;
@ -250,6 +379,12 @@ export class SettingsComponent implements OnInit {
notifyRestartNeeded(){
this.notificationService.notifyRestartNotification('Reload to apply changes');
}
openTutorial(): boolean {
localStorage.setItem('tutorial', JSON.stringify(false));
this.navigationService.closePanel();
return false;
}
}
enum ColumnShortcut {

View File

@ -8,27 +8,36 @@
<fa-icon [icon]="faSearch"></fa-icon>
</a>
<div *ngFor="let account of accounts">
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
(openMenuNotify)="onOpenMenuNotify($event)">
</app-account-icon>
<div *ngIf="!iconMenuIsDraggable">
<div *ngFor="let account of accounts">
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
(openMenuNotify)="onOpenMenuNotify($event)">
</app-account-icon>
</div>
</div>
<div *ngIf="iconMenuIsDraggable" cdkDropList [cdkDropListData]="accounts" (cdkDropListDropped)="onDrop($event)">
<div *ngFor="let account of accounts" cdkDrag class="draggable">
<fa-icon class="draggable__icon" [icon]="faArrowsAltV"></fa-icon>
<img class="draggable__avatar" src="{{ account.avatar }}" />
</div>
</div>
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }"
href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
<fa-icon [icon]="faPlus"></fa-icon>
</a>
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
*ngIf="hasAccounts && hasScheduledStatuses"
(click)="openScheduledStatuses()"
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href
title="scheduled statuses" *ngIf="hasAccounts && hasScheduledStatuses" (click)="openScheduledStatuses()"
(contextmenu)="openScheduledStatuses()">
<fa-icon [icon]="faCalendarAlt"></fa-icon>
</a>
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
(contextmenu)="openSettings()">
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings"
(click)="openSettings()" (contextmenu)="openSettings()">
<fa-icon [icon]="faCog"></fa-icon>
</a>
</div>

View File

@ -82,4 +82,38 @@ $height-button: 40px;
.no-accounts {
padding-top: 10px;
// color: cornflowerblue;
}
$draggable-accent-color: #47e927;
// $draggable-accent-color: #a8ff97;
.draggable {
width: 40px;
height: 40px;
margin: auto;
margin-bottom: 5px;
border: 2px solid #df0adf;
border: 2px solid $draggable-accent-color;
border-radius: 2px;
position: relative;
&__avatar {
width: calc(100%);
opacity: .8;
}
&__icon {
position: absolute;
float: left;
z-index: 5;
color:$draggable-accent-color;
top: 6px;
left: 12px;
font-size: 18px;
}
}

View File

@ -1,16 +1,18 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
import { Subscription, Observable } from "rxjs";
import { Store } from "@ngxs/store";
import { faPlus, faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
import { faPlus, faCog, faSearch, faArrowsAltV } from "@fortawesome/free-solid-svg-icons";
import { faCommentAlt, faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
import { AccountWrapper } from "../../models/account.models";
import { AccountInfo, SelectAccount } from "../../states/accounts.state";
import { AccountInfo, ReorderAccounts, SelectAccount } from "../../states/accounts.state";
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
import { ToolsService } from '../../services/tools.service';
import { ScheduledStatusService, ScheduledStatusNotification } from '../../services/scheduled-status.service';
import { SettingsService } from '../../services/settings.service';
@Component({
selector: "app-left-side-bar",
@ -23,6 +25,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
faPlus = faPlus;
faCog = faCog;
faCalendarAlt = faCalendarAlt;
faArrowsAltV = faArrowsAltV;
accounts: AccountWithNotificationWrapper[] = [];
hasAccounts: boolean;
@ -32,8 +35,10 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
private accountSub: Subscription;
private scheduledSub: Subscription;
private notificationSub: Subscription;
private draggableIconMenuSub: Subscription;
constructor(
private readonly settingsService: SettingsService,
private readonly hotkeysService: HotkeysService,
private readonly scheduledStatusService: ScheduledStatusService,
private readonly toolsService: ToolsService,
@ -101,7 +106,13 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
}
}
iconMenuIsDraggable = false;
ngOnInit() {
this.draggableIconMenuSub = this.navigationService.enableDraggableIconMenu.subscribe(x => {
this.iconMenuIsDraggable = x;
});
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (accounts) {
//Update and Add
@ -133,7 +144,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
});
this.notificationSub = this.userNotificationServiceService.userNotifications.subscribe((notifications: UserNotification[]) => {
const settings = this.toolsService.getSettings();
const settings = this.settingsService.getSettings();
notifications.forEach((notification: UserNotification) => {
const acc = this.accounts.find(x => x.info.id === notification.account.id);
@ -162,6 +173,17 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.accountSub.unsubscribe();
this.notificationSub.unsubscribe();
this.scheduledSub.unsubscribe();
this.draggableIconMenuSub.unsubscribe();
}
onDrop(event: CdkDragDrop<AccountWithNotificationWrapper[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data,
event.previousIndex,
event.currentIndex);
this.store.dispatch([new ReorderAccounts(this.accounts.map(x => x.info))])
}
}
onToogleAccountNotify(acc: AccountWrapper) {

View File

@ -1,7 +1,8 @@
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener } from '@angular/core';
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener, OnDestroy } from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';
import { faTimes, faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { Subject } from 'rxjs';
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
import { OpenMediaEvent } from '../../models/common.model';
import { Attachment, PleromaAttachment } from '../../services/models/mastodon.interfaces';
@ -12,7 +13,7 @@ import { Attachment, PleromaAttachment } from '../../services/models/mastodon.in
templateUrl: './media-viewer.component.html',
styleUrls: ['./media-viewer.component.scss']
})
export class MediaViewerComponent implements OnInit {
export class MediaViewerComponent implements OnInit, OnDestroy {
private _mediaEvent: OpenMediaEvent;
faTimes = faTimes;
faAngleLeft = faAngleLeft;
@ -64,9 +65,19 @@ export class MediaViewerComponent implements OnInit {
}
}
constructor() { }
private escapeHotkey = new Hotkey('escape', (event: KeyboardEvent): boolean => {
this.close();
return false;
});
constructor(private readonly hotkeysService: HotkeysService) { }
ngOnInit() {
this.hotkeysService.add(this.escapeHotkey);
}
ngOnDestroy(): void {
this.hotkeysService.remove(this.escapeHotkey);
}
private setBrowsing() {

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { NotificationService, NotificatioData } from '../../services/notification.service';
import { NotificationService, NotificationData } from '../../services/notification.service';
@Component({
selector: 'app-notification-hub',
@ -12,7 +12,7 @@ export class NotificationHubComponent implements OnInit {
constructor(private notificationService: NotificationService) { }
ngOnInit() {
this.notificationService.notifactionStream.subscribe((notification: NotificatioData) => {
this.notificationService.notifactionStream.subscribe((notification: NotificationData) => {
let alreadyExistingNotification = this.notifications.find(x => x.avatar === notification.avatar && x.message === notification.message);
if(alreadyExistingNotification){
@ -40,13 +40,13 @@ export class NotificationHubComponent implements OnInit {
// }, 1500);
// }
onClick(notification: NotificatioData): void{
onClick(notification: NotificationData): void{
this.notifications = this.notifications.filter(x => x.id !== notification.id);
}
}
class NotificationWrapper extends NotificatioData {
constructor(data: NotificatioData) {
class NotificationWrapper extends NotificationData {
constructor(data: NotificationData) {
super(data.avatar, data.errorCode, data.message, data.isError);
}

View File

@ -2,15 +2,16 @@
<div class="hashtag-header">
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board">add column</button>
<button *ngIf="isHashtagFollowingAvailable && !isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="followThisHashtag($event)" title="follow hashtag" [disabled]="followingLoading">follow</button>
<button *ngIf="isHashtagFollowingAvailable && isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="unfollowThisHashtag($event)" title="unfollow hashtag" [disabled]="unfollowingLoading">unfollow</button>
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board" [hidden]="columnAdded">add column</button>
</a>
</div>
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
[streamElement]="hashtagElement"
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
[streamElement]="hashtagElement"
[goToTop]="goToTopSubject.asObservable()"
[userLocked]="false"
(browseAccountEvent)="browseAccount($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
</div>

View File

@ -40,6 +40,14 @@ $inner-column-size: 320px;
border: 1px solid black;
color: white;
}
&__follow-button {
position: absolute;
top: 7px;
right: 114px;
padding: 0 10px 0 10px;
border: 1px solid black;
color: white;
}
}
.hashtag-stream {

View File

@ -1,11 +1,12 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { Subject, Subscription, Observable } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
import { AccountInfo } from '../../../states/accounts.state';
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
@Component({
selector: 'app-hashtag',
@ -21,7 +22,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private _hashtagElement: StreamElement;
@Input()
@Input()
set hashtagElement(hashtagElement: StreamElement){
this._hashtagElement = hashtagElement;
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
@ -29,7 +30,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
get hashtagElement(): StreamElement{
return this._hashtagElement;
}
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
@ -38,10 +39,25 @@ export class HashtagComponent implements OnInit, OnDestroy {
private lastUsedAccount: AccountInfo;
private refreshSubscription: Subscription;
private goToTopSubscription: Subscription;
isHashtagFollowingAvailable: boolean;
isFollowingHashtag: boolean;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
followingLoading: boolean;
unfollowingLoading: boolean;
columnAdded: boolean;
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
if(this.refreshEventEmitter) {
@ -55,11 +71,22 @@ export class HashtagComponent implements OnInit, OnDestroy {
this.goToTop();
})
}
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
const selectedAccounts = accounts.filter(x => x.isSelected);
if (selectedAccounts.length > 0) {
this.lastUsedAccount = selectedAccounts[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
}
});
}
ngOnDestroy(): void {
if(this.refreshSubscription) this.refreshSubscription.unsubscribe();
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
if (this.accountSub) this.accountSub.unsubscribe();
}
goToTop(): boolean {
@ -74,11 +101,17 @@ export class HashtagComponent implements OnInit, OnDestroy {
const newStream = new StreamElement(StreamTypeEnum.tag, `${hashtag}`, this.lastUsedAccount.id, hashtag, null, null, this.lastUsedAccount.instance);
this.store.dispatch([new AddStream(newStream)]);
this.columnAdded = true;
return false;
}
refresh(): any {
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
if (this.isHashtagFollowingAvailable) {
this.checkIfFollowingHashtag(this.lastUsedAccount);
}
this.appStreamStatuses.refresh();
}
@ -95,4 +128,41 @@ export class HashtagComponent implements OnInit, OnDestroy {
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
private updateHashtagFollowStatus(account: AccountInfo): void {
this.toolsService.getInstanceInfo(account).then(instanceInfo => {
if (instanceInfo.major >= 4) {
this.isHashtagFollowingAvailable = true;
this.checkIfFollowingHashtag(account);
} else {
this.isHashtagFollowingAvailable = false;
}
});
}
private checkIfFollowingHashtag(account: AccountInfo): void {
this.mastodonService.getHashtag(account, this.hashtagElement.tag).then(tag => {
this.isFollowingHashtag = tag.following;
});
}
followThisHashtag(event): boolean {
this.followingLoading = true;
event.stopPropagation();
this.mastodonService.followHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => {
this.isFollowingHashtag = tag.following;
this.followingLoading = false;
});
return false
}
unfollowThisHashtag(event): boolean {
this.unfollowingLoading = true;
event.stopPropagation();
this.mastodonService.unfollowHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => {
this.isFollowingHashtag = tag.following;
this.unfollowingLoading = false;
});
return false
}
}

View File

@ -102,13 +102,13 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.statusStateSub = this.statusStateService.stateNotification.subscribe((state: StatusState) => {
if (state && state.statusId === this.displayedStatus.url) {
if (state.isFavorited) {
if (state.isFavorited !== null) {
this.favoriteStatePerAccountId[state.accountId] = state.isFavorited;
}
if (state.isRebloged) {
if (state.isRebloged !== null) {
this.bootedStatePerAccountId[state.accountId] = state.isRebloged;
}
if (state.isBookmarked) {
if (state.isBookmarked !== null) {
this.bookmarkStatePerAccountId[state.accountId] = state.isBookmarked;
}
@ -138,7 +138,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
private checkStatus(accounts: AccountInfo[]): void {
const status = this.statusWrapper.status;
const provider = this.statusWrapper.provider;
this.selectedAccounts = accounts.filter(x => x.isSelected);
this.selectedAccounts = accounts.filter(x => x.isSelected);
if (!this.statusWrapper.isRemote) {
this.isProviderSelected = this.selectedAccounts.filter(x => x.id === provider.id).length > 0;
@ -184,13 +184,18 @@ export class ActionBarComponent implements OnInit, OnDestroy {
return false;
}
private boostPromise: Promise<any>;
boost(): boolean {
if (this.boostIsLoading) return;
if (!this.boostPromise) {
this.boostPromise = Promise.resolve(true);
}
this.boostIsLoading = true;
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
this.boostPromise = this.boostPromise
.then(() => {
this.boostIsLoading = true;
return this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
})
.then((status: Status) => {
if (this.isBoosted && status.reblogged) {
return this.mastodonService.unreblog(account, status);
@ -219,18 +224,24 @@ export class ActionBarComponent implements OnInit, OnDestroy {
.then(() => {
this.statusStateService.statusReblogStatusChanged(this.displayedStatus.url, account.id, this.bootedStatePerAccountId[account.id]);
this.boostIsLoading = false;
this.boostPromise = null;
});
return false;
}
private favoritePromise: Promise<any>;
favorite(): boolean {
if (this.favoriteIsLoading) return;
if (!this.favoritePromise) {
this.favoritePromise = Promise.resolve(true);
}
this.favoriteIsLoading = true;
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
this.favoritePromise = this.favoritePromise
.then(() => {
this.favoriteIsLoading = true;
return this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
})
.then((status: Status) => {
if (this.isFavorited && status.favourited) {
return this.mastodonService.unfavorite(account, status);
@ -254,19 +265,24 @@ export class ActionBarComponent implements OnInit, OnDestroy {
.then(() => {
this.statusStateService.statusFavoriteStatusChanged(this.displayedStatus.url, account.id, this.favoriteStatePerAccountId[account.id]);
this.favoriteIsLoading = false;
this.favoritePromise = null;
});
return false;
}
private bookmarkPromise: Promise<any>;
bookmark(): boolean {
if (this.bookmarkingIsLoading) return;
this.bookmarkingIsLoading = true;
if (!this.bookmarkPromise) {
this.bookmarkPromise = Promise.resolve(true);
}
const account = this.toolsService.getSelectedAccounts()[0];
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
this.bookmarkPromise = this.bookmarkPromise
.then(() => {
this.bookmarkingIsLoading = true;
return this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
})
.then((status: Status) => {
if (this.isBookmarked && status.bookmarked) {
return this.mastodonService.unbookmark(account, status);
@ -290,15 +306,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
.then(() => {
this.statusStateService.statusBookmarkStatusChanged(this.displayedStatus.url, account.id, this.bookmarkStatePerAccountId[account.id]);
this.bookmarkingIsLoading = false;
this.bookmarkPromise = null;
});
// setTimeout(() => {
// this.isBookmarked = !this.isBookmarked;
// this.bookmarkingIsLoading = false;
// }, 2000);
return false;
}
@ -332,13 +342,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}
private checkIfBookmarksAreAvailable(account: AccountInfo) {
this.toolsService.getInstanceInfo(account)
.then((instance: InstanceInfo) => {
if (instance.major >= 3 && instance.minor >= 1) {
this.isBookmarksAvailable = true;
} else {
this.isBookmarksAvailable = false;
}
this.toolsService.isBookmarksAreAvailable(account)
.then((isAvailable: boolean) => {
this.isBookmarksAvailable = isAvailable;
})
.catch(err => {
this.isBookmarksAvailable = false;

View File

@ -1,4 +1,4 @@
<a href class="context-menu-link" (click)="onContextMenu($event)"
<a href class="context-menu-link" (click)="onContextMenu($event)"
[class.context-menu-link__status]="statusWrapper"
[class.context-menu-link__profile]="displayedAccount"
title="More">
@ -27,19 +27,42 @@
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.muted">
Unmute conversation
</ng-template>
<ng-template contextMenuItem divider="true"></ng-template>
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected">
<ng-template contextMenuItem (execute)="hideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && this.relationship.showing_reblogs">
Hide boosts from @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="unhideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && !this.relationship.showing_reblogs">
Unhide boosts from @{{ this.username }}
</ng-template>
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.muting">
Mute @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected">
Block @{{ this.username }}
<ng-template contextMenuItem (execute)="unmuteAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.muting">
Unmute @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.blocking">
Block @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="unblockAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.blocking">
Unblock @{{ this.username }}
</ng-template>
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
<ng-template contextMenuItem (execute)="blockDomain()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.domain_blocking">
Block domain {{ this.domain }}
</ng-template>
<ng-template contextMenuItem (execute)="unblockDomain()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.domain_blocking">
Unblock domain {{ this.domain }}
</ng-template>
<ng-template contextMenuItem divider="true" *ngIf="isOwnerSelected"></ng-template>
<ng-template contextMenuItem (execute)="pinOnProfile()" *ngIf="statusWrapper && isOwnerSelected && !displayedStatus.pinned && displayedStatus.visibility === 'public'">
Pin on profile
</ng-template>
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
Unpin from profile
</ng-template>
<ng-template contextMenuItem (execute)="edit()" *ngIf="statusWrapper && isOwnerSelected && isEditingAvailable">
Edit
</ng-template>
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="statusWrapper && isOwnerSelected">
Delete
</ng-template>

View File

@ -4,8 +4,8 @@ import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu';
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { Status, Account, Results } from '../../../../../services/models/mastodon.interfaces';
import { ToolsService, OpenThreadEvent } from '../../../../../services/tools.service';
import { Status, Account, Results, Relationship } from '../../../../../services/models/mastodon.interfaces';
import { ToolsService, OpenThreadEvent, InstanceInfo } from '../../../../../services/tools.service';
import { StatusWrapper } from '../../../../../models/common.model';
import { NavigationService } from '../../../../../services/navigation.service';
import { AccountInfo } from '../../../../../states/accounts.state';
@ -25,12 +25,17 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
private loadedAccounts: AccountInfo[];
displayedStatus: Status;
username: string;
domain: string;
isOwnerSelected: boolean;
isEditingAvailable: boolean;
@Input() statusWrapper: StatusWrapper;
@Input() displayedAccount: Account;
@Input() relationship: Relationship;
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Output() relationshipChanged = new EventEmitter<Relationship>();
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
@ -70,6 +75,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
}
this.username = account.acct.split('@')[0];
this.domain = account.acct.split('@')[1];
this.fullHandle = this.toolsService.getAccountFullHandle(account);
}
@ -78,6 +84,14 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.isOwnerSelected = selectedAccount.username.toLowerCase() === this.displayedStatus.account.username.toLowerCase()
&& selectedAccount.instance.toLowerCase() === this.displayedStatus.account.url.replace('https://', '').split('/')[0].toLowerCase();
this.toolsService.getInstanceInfo(selectedAccount).then((instanceInfo: InstanceInfo) => {
if (instanceInfo.major >= 4) {
this.isEditingAvailable = true;
} else {
this.isEditingAvailable = false;
}
});
}
@ -155,38 +169,139 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
return false;
}
hideBoosts(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.hideBoosts(acc, target);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
unhideBoosts(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.unhideBoosts(acc, target);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
muteAccount(): boolean {
this.loadedAccounts.forEach(acc => {
this.toolsService.findAccount(acc, this.fullHandle)
.then((target: Account) => {
this.mastodonService.mute(acc, target.id);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
});
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.mute(acc, target.id);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
unmuteAccount(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.unmute(acc, target.id);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
return target;
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
blockAccount(): boolean {
this.loadedAccounts.forEach(acc => {
this.toolsService.findAccount(acc, this.fullHandle)
.then((target: Account) => {
this.mastodonService.block(acc, target.id);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.block(acc, target.id);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
unblockAccount(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.unblock(acc, target.id);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
return target;
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
blockDomain(): boolean {
const response = confirm(`Are you really sure you want to block the entire ${this.domain} domain? You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.`);
if (response) {
const acc = this.toolsService.getSelectedAccounts()[0];
this.mastodonService.blockDomain(acc, this.domain)
.then(_ => {
this.relationship.domain_blocking = true;
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
});
}
return false;
}
unblockDomain(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.mastodonService.blockDomain(acc, this.domain)
.then(_ => {
this.relationship.domain_blocking = false;
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
@ -282,6 +397,18 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
return false;
}
edit(): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
this.getStatus(selectedAccount)
.then(() => {
this.navigationService.edit(this.statusWrapper);
})
.catch(err => {
this.notificationService.notifyHttpError(err, selectedAccount);
});
return false;
}
private getStatus(account: AccountInfo): Promise<Status> {
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.status);

View File

@ -1,14 +1,18 @@
<div class="image">
<div class="image">
<div class="image__alt" *ngIf="displayAltLabel && attachment.description" title="{{ attachment.description }}">ALT</div>
<a *ngIf="status" href class="image__status" (click)="openStatus()" (auxclick)="openStatus()" title="open status">
<fa-icon class="image__status--icon" [icon]="faExternalLinkAlt"></fa-icon>
</a>
<a href class="image__link" (click)="openExternal()" (auxclick)="openExternal()" title="open image">
<fa-icon class="image__link--icon" [icon]="faLink"></fa-icon>
</a>
<a *ngIf="attachment.type === 'image'" class="galery__image--link" title="{{ attachment.description }}"
(click)="attachmentSelected()">
<img class="galery__image--1" src="{{ attachment.preview_url }}" />
<img class="galery__image--1" src="{{ attachment.preview_url | ensureHttps }}" />
</a>
<video *ngIf="attachment.type === 'gifv'" class="galery__image--link galery__image--1" role="application" loop
autoplay (click)="attachmentSelected()">
<source src="{{ attachment.url }}" type="video/mp4">
<source src="{{ attachment.url | ensureHttps }}" type="video/mp4">
</video>
</div>

View File

@ -25,10 +25,48 @@
// }
}
&__status {
z-index: 10;
position: absolute;
top: 0;
right: 25px;
padding: 5px 5px 8px 8px;
transition: all .2s;
opacity: 0;
color: white;
&--icon {
filter: drop-shadow(0 0 3px rgb(78, 78, 78));
}
}
&:hover &__link {
opacity: 1;
cursor: pointer;
}
&:hover &__status {
opacity: 1;
cursor: pointer;
}
&__alt {
display: inline;
color: white;
z-index: 10;
position: absolute;
bottom: 5px;
left: 5px;
font-size: 10px;
font-weight: bolder;
background-color: rgba($color: #000000, $alpha: 0.5);
border-radius: 3px;
padding: 2px 5px;
}
}
img,

View File

@ -1,7 +1,10 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { faLink } from "@fortawesome/free-solid-svg-icons";
import { faLink, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { SettingsService } from '../../../../../services/settings.service';
import { Attachment } from '../../../../../services/models/mastodon.interfaces';
import { StatusWrapper } from '../../../../../models/common.model';
import { OpenThreadEvent } from '../../../../../services/tools.service';
@Component({
selector: 'app-attachement-image',
@ -10,11 +13,19 @@ import { Attachment } from '../../../../../services/models/mastodon.interfaces';
})
export class AttachementImageComponent implements OnInit {
faLink = faLink;
faExternalLinkAlt = faExternalLinkAlt;
displayAltLabel: boolean;
@Input() attachment: Attachment;
@Input() status: StatusWrapper;
@Output() openEvent = new EventEmitter();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
constructor() { }
constructor(
private readonly settingsService: SettingsService
) {
this.displayAltLabel = this.settingsService.getSettings().enableAltLabel;
}
ngOnInit() {
}
@ -28,4 +39,13 @@ export class AttachementImageComponent implements OnInit {
window.open(this.attachment.url, '_blank');
return false;
}
openStatus(): boolean {
if(!this.status) return false;
const openThreadEvent = new OpenThreadEvent(this.status.status, this.status.provider);
this.browseThreadEvent.next(openThreadEvent);
return false;
}
}

View File

@ -30,6 +30,10 @@ export class AttachementsComponent implements OnInit {
@Input('attachments')
set attachments(value: Attachment[]) {
this.imageAttachments = [];
this.videoAttachments = [];
this.audioAttachments = [];
this._attachments = value;
this.setAttachments(value);
}

View File

@ -1,6 +1,6 @@
<div class="card-data" *ngIf="card.type === 'link' || card.type === 'video'">
<a *ngIf="card.type === 'link'" class="card-data__link" href="{{ card.url }}" target="_blank" title="{{ card.title }} &#10;{{ host }}">
<img *ngIf="card.image" class="card-data__link--image" src="{{ card.image }}" alt="" />
<a *ngIf="card.type === 'link'" class="card-data__link" href="{{ card.url }}" target="_blank" rel="noopener noreferrer" title="{{ card.title }} &#10;{{ host }}">
<img *ngIf="card.image" class="card-data__link--image" src="{{ card.image | ensureHttps }}" alt="" />
<div *ngIf="!card.image" class="card-data__link--image">
<fa-icon class="card-data__link--image--logo" [icon]="faFileAlt"></fa-icon>
</div>
@ -24,7 +24,7 @@
<fa-icon [icon]="faExternalLinkAlt"></fa-icon>
</a>
</div>
<img src="{{ card.image }}" class="card-data__video--preview--image" />
<img src="{{ card.image | ensureHttps }}" class="card-data__video--preview--image" />
</div>
<div #video *ngIf="showHtml" class="card-data__video--content" [innerHTML]="html">

View File

@ -22,9 +22,9 @@ $expand-color: $column-color;
right: 0;
padding-top: 60px;
padding-left: 15px;
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba($expand-color ,0.25), rgba($expand-color,0.5), $expand-color, $expand-color);
&--link{
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba($expand-color, 0.25), rgba($expand-color, 0.5), $expand-color, $expand-color);
&--link {
transition: all .2s;
color: #a9b5d8;
color: #c0c8e0;
@ -41,9 +41,9 @@ $expand-color: $column-color;
}
&--selected {
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba($selected-status ,0.25), rgba($selected-status,0.5), $selected-status, $selected-status);
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba($selected-status, 0.25), rgba($selected-status, 0.5), $selected-status, $selected-status);
}
}
}
}
@ -51,20 +51,53 @@ $expand-color: $column-color;
:host ::ng-deep .content {
// font-size: 14px;
color: $status-primary-color;
& a,
.mention,
.ellipsis {
color: $status-links-color;
}
& .invisible {
display: none;
}
& p {
margin: 0px;
white-space: pre-wrap;
//font-size: .9em;
// font-size: 14px;
}
& p:not(:last-child) {
margin-bottom: 20px;
}
& code {
color: #a0d1ff;
}
& pre {
padding: 0 5px 5px 5px;
background-color: #000000;
scrollbar-width: thin;
border-radius: 5px 5px 0 0;
&::-webkit-scrollbar {
width: $scroll-bar-width;
height: $scroll-bar-width;
}
&::-webkit-scrollbar-thumb {
-webkit-border-radius: 0px;
border-radius: 0px;
background: $scrollbar-color-thumb;
background: #384958;
}
}
img {
max-width: 100%;
}
}

View File

@ -42,7 +42,17 @@ describe('DatabindedTextComponent', () => {
const url = 'https://test.social/tags/programmers';
const sample = `<p>bla1 <a href="${url}" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>${hashtag}</span></a> bla2</p>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="hashtag-programmers" title="#programmers">#programmers</a>');
expect(component.processedText).toContain(`<a href="${url}" class="hashtag-programmers" title="#programmers" target="_blank" rel="noopener noreferrer">#programmers</a>`);
expect(component.processedText).toContain('bla1');
expect(component.processedText).toContain('bla2');
});
it('should parse hashtag - Hometown', () => {
const hashtag = 'MicroFiction';
const url = 'https://mastodon.social/tags/MicroFiction';
const sample = `<p>"bla1"<br><a href="${url}" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span class="article-type">${hashtag}</span></a> bla2</p>`;
component.text = sample;
expect(component.processedText).toContain(`<a href="${url}" class="hashtag-${hashtag}" title="#${hashtag}" target="_blank" rel="noopener noreferrer">#${hashtag}</a>`);
expect(component.processedText).toContain('bla1');
expect(component.processedText).toContain('bla2');
});
@ -50,7 +60,7 @@ describe('DatabindedTextComponent', () => {
it('should parse hashtag - Pleroma 2.0.2', () => {
const sample = `Blabla <a class="hashtag" data-tag="covid19" href="https://url.com/tag/covid19">#covid19</a> Blibli`;
component.text = sample;
expect(component.processedText).toContain('<a href class="hashtag-covid19" title="#covid19">#covid19</a>');
expect(component.processedText).toContain(`<a href="https://url.com/tag/covid19" class="hashtag-covid19" title="#covid19" target="_blank" rel="noopener noreferrer">#covid19</a>`);
expect(component.processedText).toContain('Blabla');
expect(component.processedText).toContain('Blibli');
});
@ -60,16 +70,32 @@ describe('DatabindedTextComponent', () => {
const url = 'https://mastodon.social/@sengi_app';
const sample = `<p>bla1 <span class="h-card"><a href="${url}" class="u-url mention" rel="nofollow noopener" target="_blank">@<span>${mention}</span></a></span> bla2</p>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social">@sengi_app</a>');
expect(component.processedText).toContain(`<a href="${url}" class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social" target="_blank" rel="noopener noreferrer">@sengi_app</a>`);
expect(component.processedText).toContain('bla1');
expect(component.processedText).toContain('bla2');
});
it('should parse remote mention', () => {
const sample = `<p><span class="article-type"><a href="https://domain.name/@username" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@<span class="article-type">username</span></a></span> <br>Yes, indeed.</p>`;
component.text = sample;
expect(component.processedText).toBe('<p><span class="article-type"><a href="https://domain.name/@username" class="account--username-domain-name" title="@username@domain.name" target="_blank" rel="noopener noreferrer">@username</a> <br>Yes, indeed.</p>');
});
it('should parse link', () => {
const url = 'mydomain.co/test';
const sample = `<p>bla1 <a href="https://${url}" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">${url}</span><span class="invisible"></span></a> bla2</p>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="link-httpsmydomaincotest" title="open link">mydomain.co/test</a>');
expect(component.processedText).toContain(`<a href="https://${url}" class="link-httpsmydomaincotest" title="open link" target="_blank" rel="noopener noreferrer">mydomain.co/test</a>`);
expect(component.processedText).toContain('bla1');
expect(component.processedText).toContain('bla2');
});
it('should parse link - Hometown', () => {
const url = 'bbs.archlinux.org/test';
const sample = `<p>bla1 <a href="https://${url}" rel="nofollow noopener noreferrer" target="_blank"><span class="article-type">https://</span><span class="article-type">${url}</span><span class="article-type">p?id=264086&amp;action=new</span></a> bla2`;
component.text = sample;
expect(component.processedText).toContain(`<a href="https://${url}" class="link-httpsbbsarchlinuxorgtest" title="open link" target="_blank" rel="noopener noreferrer">${url}</a>`);
expect(component.processedText).toContain('bla1');
expect(component.processedText).toContain('bla2');
});
@ -78,21 +104,21 @@ describe('DatabindedTextComponent', () => {
const url = 'bbc.com/news/magazine-34901704';
const sample = `<p>The rise of"<br><a href="https:www//${url}" rel="nofollow noopener" target="_blank"><span class="invisible">https://www.</span><span class="">${url}</span><span class="invisible"></span></a></p>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="link-httpswwwbbccomnewsmagazine34901704" title="open link">bbc.com/news/magazine-34901704</a></p>');
expect(component.processedText).toContain(`<a href="https:www//${url}" class="link-httpswwwbbccomnewsmagazine34901704" title="open link" target="_blank" rel="noopener noreferrer">bbc.com/news/magazine-34901704</a></p>`);
});
it('should parse link - dual section', () => {
const sample = `<p>Test.<br><a href="https://peertube.fr/videos/watch/69bb6e90-ec0f-49a3-9e28-41792f4a7c5f" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">peertube.fr/videos/watch/69bb6</span><span class="invisible">e90-ec0f-49a3-9e28-41792f4a7c5f</span></a></p>`;
component.text = sample;
expect(component.processedText).toContain('<p>Test.<br><a href class="link-httpspeertubefrvideoswatch69bb6e90ec0f49a39e2841792f4a7c5f" title="open link">peertube.fr/videos/watch/69bb6</a></p>');
expect(component.processedText).toContain('<p>Test.<br><a href="https://peertube.fr/videos/watch/69bb6e90-ec0f-49a3-9e28-41792f4a7c5f" class="link-httpspeertubefrvideoswatch69bb6e90ec0f49a39e2841792f4a7c5f" title="open link" target="_blank" rel="noopener noreferrer">peertube.fr/videos/watch/69bb6</a></p>');
});
it('should parse link with special character', () => {
const sample = `<p>Magnitude: 2.5 Depth: 3.4 km<br>Details: 2018/09/27 06:50:17 34.968N 120.685W<br>Location: 10 km (6 mi) W of Guadalupe, CA<br>Map: <a href="https://www.google.com/maps/place/34°58'4%20N+120°41'6%20W/@34.968,-120.685,10z" rel="noopener" target="_blank" class="status-link" title="https://www.google.com/maps/place/34%C2%B058'4%20N+120%C2%B041'6%20W/@34.968,-120.685,10z"><span class="invisible">https://www.</span><span class="ellipsis">google.com/maps/place/34°58'4%</span><span class="invisible">20N+120°41'6%20W/@34.968,-120.685,10z</span></a><br><a href="https://mastodon.cloud/tags/earthquake" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>EarthQuake</span></a> <a href="https://mastodon.cloud/tags/quake" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>Quake</span></a> <a href="https://mastodon.cloud/tags/california" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>California</span></a></p>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="link-httpswwwgooglecommapsplace3458420N12041620W3496812068510z" title="open link">google.com/maps/place/34°58\'4%</a>');
expect(component.processedText).toContain(`<a href="https://www.google.com/maps/place/34°58'4%20N+120°41'6%20W/@34.968,-120.685,10z" class="link-httpswwwgooglecommapsplace3458420N12041620W3496812068510z" title="open link" target="_blank" rel="noopener noreferrer">google.com/maps/place/34°58\'4%</a>`);
});
it('should parse combined hashtag, mention and link', () => {
@ -103,9 +129,9 @@ describe('DatabindedTextComponent', () => {
const linkUrl = 'mydomain.co/test';
const sample = `<p>bla1 <a href="${hashtagUrl}" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>${hashtag}</span></a> bla2 <span class="h-card"><a href="${mentionUrl}" class="u-url mention" rel="nofollow noopener" target="_blank">@<span>${mention}</span></a></span> bla3 <a href="https://${linkUrl}" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">${linkUrl}</span><span class="invisible"></span></a> bla4</p>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="hashtag-programmers" title="#programmers">#programmers</a>');
expect(component.processedText).toContain('<a href class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social">@sengi_app</a>');
expect(component.processedText).toContain('<a href class="link-httpsmydomaincotest" title="open link">mydomain.co/test</a>');
expect(component.processedText).toContain(`<a href="${hashtagUrl}" class="hashtag-programmers" title="#programmers" target="_blank" rel="noopener noreferrer">#programmers</a>`);
expect(component.processedText).toContain(`<a href="${mentionUrl}" class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social" target="_blank" rel="noopener noreferrer">@sengi_app</a>`);
expect(component.processedText).toContain(`<a href="https://${linkUrl}" class="link-httpsmydomaincotest" title="open link" target="_blank" rel="noopener noreferrer">mydomain.co/test</a>`);
expect(component.processedText).toContain('bla1');
expect(component.processedText).toContain('bla2');
expect(component.processedText).toContain('bla3');
@ -116,7 +142,7 @@ describe('DatabindedTextComponent', () => {
const sample = `bla1 <a href="https://www.lemonde.fr/planete.html?xtor=RSS-3208" rel="nofollow noopener" class="" target="_blank">https://social.bitcast.info/url/819438</a>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="link-httpswwwlemondefrplanetehtmlxtorRSS3208" title="open link">https://social.bitcast.info/url/819438</a>');
expect(component.processedText).toContain('<a href="https://www.lemonde.fr/planete.html?xtor=RSS-3208" class="link-httpswwwlemondefrplanetehtmlxtorRSS3208" title="open link" target="_blank" rel="noopener noreferrer">https://social.bitcast.info/url/819438</a>');
expect(component.processedText).toContain('bla1');
});
@ -124,49 +150,79 @@ describe('DatabindedTextComponent', () => {
const sample = `<div>bla1 <br> @<a href="https://instance.club/user/1" class="h-card mention status-link" rel="noopener" target="_blank" title="https://instance.club/user/1">user</a>&nbsp;</div>`;
component.text = sample;
expect(component.processedText).toContain('<a href class="account--user-instance-club" title="@user@instance.club">@user</a>');
expect(component.processedText).toContain('<a href="https://instance.club/user/1" class="account--user-instance-club" title="@user@instance.club" target="_blank" rel="noopener noreferrer">@user</a>');
expect(component.processedText).toContain('bla1');
})
});
it('should parse mention - Pleroma in Mastodon - 2', () => {
const sample = `<div><span><a class="mention status-link" href="https://pleroma.site/users/kaniini" rel="noopener" target="_blank" title="kaniini@pleroma.site">@<span>kaniini</span></a></span> <span><a class="mention status-link" href="https://mastodon.social/@Gargron" rel="noopener" target="_blank" title="Gargron@mastodon.social">@<span>Gargron</span></a></span> bla1?</div>`;
component.text = sample;
expect(component.processedText).toContain('<div><span><a href class="account--kaniini-pleroma-site" title="@kaniini@pleroma.site">@kaniini</a> <span><a href class="account--Gargron-mastodon-social" title="@Gargron@mastodon.social">@Gargron</a> bla1?</div>');
expect(component.processedText).toContain('<div><span><a href="https://pleroma.site/users/kaniini" class="account--kaniini-pleroma-site" title="@kaniini@pleroma.site" target="_blank" rel="noopener noreferrer">@kaniini</a> <span><a href="https://mastodon.social/@Gargron" class="account--Gargron-mastodon-social" title="@Gargron@mastodon.social" target="_blank" rel="noopener noreferrer">@Gargron</a> bla1?</div>');
});
it('should parse mention - Friendica in Mastodon', () => {
const sample = `@<span class=""><a href="https://m.s/me" class="u-url mention" rel="nofollow noopener" target="_blank"><span class="mention">me</span></a></span> Blablabla.`;
component.text = sample;
expect(component.processedText).toContain('<span class=""><a href class="account--me-m-s" title="@me@m.s">@me</a></span> Blablabla.');
expect(component.processedText).toContain('<span class=""><a href="https://m.s/me" class="account--me-m-s" title="@me@m.s" target="_blank" rel="noopener noreferrer">@me</a></span> Blablabla.');
});
it('should parse mention - Misskey in Mastodon', () => {
const sample = `<p><a href="https://mastodon.social/users/sengi_app" class="mention" rel="nofollow noopener" target="_blank">@sengi_app@mastodon.social</a><span> Blabla</span></p>`;
component.text = sample;
expect(component.processedText).toContain('<p><a href class="account--sengi_app-mastodon-social-mastodon-social" title="@sengi_app@mastodon.social@mastodon.social">@sengi_app@mastodon.social</a><span> Blabla</span></p>'); //FIXME: dont let domain appear in name
expect(component.processedText).toContain('<p><a href="https://mastodon.social/users/sengi_app" class="account--sengi_app-mastodon-social-mastodon-social" title="@sengi_app@mastodon.social@mastodon.social" target="_blank" rel="noopener noreferrer">@sengi_app@mastodon.social</a><span> Blabla</span></p>'); //FIXME: dont let domain appear in name
});
it('should parse mention - Misskey in Pleroma', () => {
const sample = `<p><a href="https://domain.xyz/@sengi" class="u-url mention">@sengi@domain.xyz</a><span> </span><a href="https://domain.eu/@sengi" class="u-url mention">@sengi@domain.eu</a><span> bla bla<br/>bla bla bla</span></p>`;
component.text = sample;
expect(component.processedText).toContain('<a href="https://domain.xyz/@sengi" class="account--sengi-domain-xyz" title="@sengi@domain.xyz" target="_blank" rel="noopener noreferrer">@sengi</a><span>');
expect(component.processedText).toContain('<a href="https://domain.eu/@sengi" class="account--sengi-domain-eu" title="@sengi@domain.eu" target="_blank" rel="noopener noreferrer">@sengi</a>');
expect(component.processedText).toContain('<span> bla bla<br/>bla bla bla</span>');
});
it('should parse mention - Misskey in Mastodon - 2', () => {
const sample = `<p><span>Since </span><a href="https://mastodon.technology/@test" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@test@mastodon.technology</a><span> mentioned </span></p>`;
component.text = sample;
expect(component.processedText).toContain('<a href="https://mastodon.technology/@test" class="account--test-mastodon-technology" title="@test@mastodon.technology" target="_blank" rel="noopener noreferrer">@test</a>');
});
it('should parse mention - Zap in Mastodon', () => {
const sample = `test @<span class="h-card"><a class="u-url mention" href="https://mastodon.social/@test" rel="nofollow noopener noreferrer" target="_blank">test</a></span> bla"`;
component.text = sample;
expect(component.processedText).toContain('test <span class="h-card"><a href="https://mastodon.social/@test" class="account--test-mastodon-social" title="@test@mastodon.social" target="_blank" rel="noopener noreferrer">@test</a></span>');
});
it('should parse hastag - Pleroma', () => {
const sample = `<p>Bla <a href="https://ubuntu.social/tags/kubecon" rel="tag">#<span>KubeCon</span></a> Bla</p>`;
component.text = sample;
expect(component.processedText).toContain('<p>Bla <a href class="hashtag-KubeCon" title="#KubeCon">#KubeCon</a> Bla</p>');
expect(component.processedText).toContain('<p>Bla <a href="https://ubuntu.social/tags/kubecon" class="hashtag-KubeCon" title="#KubeCon" target="_blank" rel="noopener noreferrer">#KubeCon</a> Bla</p>');
});
it('should parse link - Pleroma', () => {
const sample = `<p>Bla <a href="https://cloudblogs.microsoft.com/opensource/2019/05/21/service-mesh-interface-smi-release/"><span>https://</span><span>cloudblogs.microsoft.com/opens</span><span>ource/2019/05/21/service-mesh-interface-smi-release/</span></a></p>`;
component.text = sample;
expect(component.processedText).toContain('<p>Bla <a href class="link-httpscloudblogsmicrosoftcomopensource20190521servicemeshinterfacesmirelease" title="open link">cloudblogs.microsoft.com/opens</a></p>');
expect(component.processedText).toContain('<p>Bla <a href="https://cloudblogs.microsoft.com/opensource/2019/05/21/service-mesh-interface-smi-release/" class="link-httpscloudblogsmicrosoftcomopensource20190521servicemeshinterfacesmirelease" title="open link" target="_blank" rel="noopener noreferrer">cloudblogs.microsoft.com/opens</a></p>');
});
it('should parse link 2 - Pleroma', () => {
const sample = `Bla<br /><br /><a href="https://link/">https://link/</a>`;
component.text = sample;
expect(component.processedText).toContain('Bla<br /><br /><a href class="link-httpslink" title="open link">https://link/</a>');
expect(component.processedText).toContain('Bla<br /><br /><a href="https://link/" class="link-httpslink" title="open link" target="_blank" rel="noopener noreferrer">https://link/</a>');
});
it('should sanitize link', () => {
const sample = `https://domain.fr/public.php?op=rss&amp;id=-2&amp;key=60c63a21c2928546b4485017876fe850c6ebcebd#tag:domain.fr,2020-05-26:/49902061`;
let result = (<any>component).sanitizeLink(sample);
expect(result).toBe('https://domain.fr/public.php?op=rss&id=-2&key=60c63a21c2928546b4485017876fe850c6ebcebd#tag:domain.fr,2020-05-26:/49902061');
});
});

View File

@ -28,7 +28,7 @@ export class DatabindedTextComponent implements OnInit {
@Input('text')
set text(value: string) {
// console.warn(value);
// console.log(value);
let parser = new DOMParser();
var dom = parser.parseFromString(value, 'text/html')
@ -44,6 +44,10 @@ export class DatabindedTextComponent implements OnInit {
value = value.replace('class="mention" rel="nofollow noopener" target="_blank">@', 'class="mention" rel="nofollow noopener" target="_blank">'); //Misskey sanitarization
} while (value.includes('class="mention" rel="nofollow noopener" target="_blank">@'));
do {
value = value.replace('@<span class="h-card">', '<span class="h-card">'); //Zap sanitarization
} while (value.includes('@<span class="h-card">'));
let linksSections = value.split('<a ');
for (let section of linksSections) {
@ -89,10 +93,11 @@ export class DatabindedTextComponent implements OnInit {
private processHashtag(section: string) {
let extractedLinkAndNext = section.split('</a>');
let extractedHashtag = extractedLinkAndNext[0].split('#')[1].replace('<span>', '').replace('</span>', '');
let extractedHashtag = extractedLinkAndNext[0].split('#')[1].replace('<span class="article-type">', '').replace('<span>', '').replace('</span>', '');
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
let classname = this.getClassNameForHastag(extractedHashtag);
this.processedText += ` <a href class="${classname}" title="#${extractedHashtag}">#${extractedHashtag}</a>`;
this.processedText += `<a href="${extractedUrl}" class="${classname}" title="#${extractedHashtag}" target="_blank" rel="noopener noreferrer">#${extractedHashtag}</a>`;
if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1];
this.hashtags.push(extractedHashtag);
}
@ -104,6 +109,22 @@ export class DatabindedTextComponent implements OnInit {
if (section.includes('<span class="mention">')) { //Friendica
extractedAccountAndNext = section.split('</a>');
extractedAccountName = extractedAccountAndNext[0].split('<span class="mention">')[1].split('</span>')[0];
} else if (section.includes('>@<span class="article-type">')) { //Remote status
extractedAccountAndNext = section.split('</a></span>');
extractedAccountName = extractedAccountAndNext[0].split('@<span class="article-type">')[1].replace('<span>', '').replace('</span>', '');
} else if (section.includes('class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@') && !section.includes('target="_blank">@<')) { //Misskey
extractedAccountAndNext = section.split('</a>');
extractedAccountName = extractedAccountAndNext[0].split('class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@')[1];
if (extractedAccountName.includes('@'))
extractedAccountName = extractedAccountName.split('@')[0];
} else if (section.includes(' class="u-url mention">@') && !section.includes(' class="u-url mention">@<')) { //Misskey in pleroma
extractedAccountAndNext = section.split('</a>');
extractedAccountName = extractedAccountAndNext[0].split(' class="u-url mention">@')[1];
if (extractedAccountName.includes('@'))
extractedAccountName = extractedAccountName.split('@')[0];
} else if (!section.includes('@<span>')) { //GNU social
extractedAccountAndNext = section.split('</a>');
extractedAccountName = extractedAccountAndNext[0].split('>')[1];
@ -118,9 +139,10 @@ export class DatabindedTextComponent implements OnInit {
//let username = extractedAccountLink[extractedAccountLink.length - 1];
let extractedAccount = `@${extractedAccountName}@${domain}`;
let extractedUrl = section.split('href="')[1].split('"')[0];
let classname = this.getClassNameForAccount(extractedAccount);
this.processedText += `<a href class="${classname}" title="${extractedAccount}">@${extractedAccountName}</a>`;
this.processedText += `<a href="${extractedUrl}" class="${classname}" title="${extractedAccount}" target="_blank" rel="noopener noreferrer">@${extractedAccountName}</a>`;
if (extractedAccountAndNext[1])
this.processedText += extractedAccountAndNext[1];
@ -133,7 +155,7 @@ export class DatabindedTextComponent implements OnInit {
}
private processLink(section: string) {
if(!section.includes('</a>')){
if (!section.includes('</a>')) {
this.processedText += section;
return;
}
@ -141,30 +163,37 @@ export class DatabindedTextComponent implements OnInit {
let extractedLinkAndNext = section.split('</a>')
let extractedUrl = extractedLinkAndNext[0].split('"')[1];
let extractedName = '';
try {
extractedName = extractedLinkAndNext[0].split('<span class="ellipsis">')[1].split('</span>')[0];
} catch (err) {
let extractedName = '';
if(extractedLinkAndNext[0].includes('<span class="article-type">')){
extractedName = extractedLinkAndNext[0].split('<span class="article-type">')[2].split('</span>')[0];
} else {
try {
extractedName = extractedLinkAndNext[0].split(`<span class="">`)[1].split('</span>')[0];
}
catch (err) {
extractedName = extractedLinkAndNext[0].split('<span class="ellipsis">')[1].split('</span>')[0];
} catch (err) {
try {
extractedName = extractedLinkAndNext[0].split(' target="_blank">')[1].split('</span>')[0];
} catch (err) { // Pleroma
extractedName = extractedLinkAndNext[0].split(`<span class="">`)[1].split('</span>')[0];
}
catch (err) {
try {
extractedName = extractedLinkAndNext[0].split('</span><span>')[1].split('</span>')[0];
} catch (err) {
extractedName = extractedLinkAndNext[0].split('">')[1];
extractedName = extractedLinkAndNext[0].split(' target="_blank">')[1].split('</span>')[0];
} catch (err) { // Pleroma
try {
extractedName = extractedLinkAndNext[0].split('</span><span>')[1].split('</span>')[0];
} catch (err) {
extractedName = extractedLinkAndNext[0].split('">')[1];
}
}
}
}
}
this.links.push(extractedUrl);
let classname = this.getClassNameForLink(extractedUrl);
this.processedText += `<a href class="${classname}" title="open link">${extractedName}</a>`;
let sanitizedLink = this.sanitizeLink(extractedUrl);
this.processedText += `<a href="${sanitizedLink}" class="${classname}" title="open link" target="_blank" rel="noopener noreferrer">${extractedName}</a>`;
if (extractedLinkAndNext.length > 1) this.processedText += extractedLinkAndNext[1];
}
@ -176,6 +205,10 @@ export class DatabindedTextComponent implements OnInit {
}
ngAfterViewInit() {
this.processEventBindings();
}
processEventBindings(){
for (const hashtag of this.hashtags) {
let classname = this.getClassNameForHastag(hashtag);
let els = <Element[]>this.contentElement.nativeElement.querySelectorAll(`.${classname}`);
@ -210,28 +243,33 @@ export class DatabindedTextComponent implements OnInit {
let classname = this.getClassNameForLink(link);
let els = this.contentElement.nativeElement.querySelectorAll(`.${classname}`);
let sanitizedLink = this.sanitizeLink(link);
for (const el of els) {
this.renderer.listen(el, 'click', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
window.open(link, '_blank');
window.open(sanitizedLink, '_blank', 'noopener');
return false;
});
this.renderer.listen(el, 'mouseup', (event) => {
if (event.which === 2) {
event.preventDefault();
event.stopImmediatePropagation();
window.open(link, '_blank');
return false;
}
});
// this.renderer.listen(el, 'mouseup', (event) => {
// if (event.which === 2) {
// event.preventDefault();
// event.stopImmediatePropagation();
// window.open(sanitizedLink, '_blank', 'noopener');
// return false;
// }
// });
}
}
}
private sanitizeLink(link: string): string {
let res = link.replace(/&amp;/g, '&');
return res;
}
private getClassNameForHastag(value: string): string {
let res = value.replace(/[.,\/#?!@$%+\^&\*;:{}=\-_`~()]/g, "");
return `hashtag-${res}`;

View File

@ -25,6 +25,9 @@
<button href *ngIf="!poll.voted && !poll.expired && !pollLocked" class="btn btn-sm btn-custom-primary poll__btn-vote"
title="don't boo, vote!" (click)="vote()">Vote</button>
<a href class="poll__refresh" *ngIf="(poll.voted || poll.expired) && !pollLocked" title="refresh poll" (click)="refresh()">refresh</a>
<div class="poll__statistics"><span *ngIf="(poll.voted || poll.expired) && !pollLocked" class="poll__separator">·</span>{{poll.votes_count}} votes<span *ngIf="!poll.expired" class="poll__separator" title="{{ poll.expires_at | timeLeft | async }}">· {{ poll.expires_at | timeLeft | async }}</span></div>
<div class="poll__statistics"><span *ngIf="(poll.voted || poll.expired) && !pollLocked && !(poll.voters_count && poll.voters_count > 0)" class="poll__separator">·</span><span *ngIf="!(poll.voters_count && poll.voters_count > 0)">{{poll.votes_count}} votes</span><span *ngIf="poll.voters_count > 0 && !pollLocked" class="poll__separator">·</span><span *ngIf="poll.voters_count > 0">{{poll.voters_count}} people</span><span *ngIf="!poll.expired" class="poll__separator" title="{{ poll.expires_at | timeLeft | async }}">· {{ poll.expires_at | timeLeft | async }}</span></div>
</div>
<div class="poll__error" *ngIf="errorOccuredWhenRetrievingPoll">
Error occured when retrieving the poll
</div>
</div>

View File

@ -27,6 +27,11 @@
margin-top: 10px;
}
&__error {
font-size: 12px;
color: red;
}
&__refresh {
font-size: 0.8em;
color: rgb(101, 121, 160);

View File

@ -22,6 +22,8 @@ export class PollComponent implements OnInit {
choiceType: string;
pollLocked: boolean;
errorOccuredWhenRetrievingPoll: boolean;
private pollSelection: number[] = [];
options: PollOptionWrapper[] = [];
@ -30,7 +32,7 @@ export class PollComponent implements OnInit {
private _poll: Poll;
@Input('poll')
set poll(value: Poll) {
if(!value) return;
if (!value) return;
this._poll = value;
@ -43,10 +45,18 @@ export class PollComponent implements OnInit {
}
this.options.length = 0;
const maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
let maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
if(!this.poll.multiple){ //Fix for absurd values in pleroma
this.poll.voters_count = this.poll.votes_count;
} else if(this.poll.voters_count * this.poll.options.length < this.poll.votes_count){
this.poll.voters_count = this.poll.votes_count;
}
let i = 0;
for (let opt of this.poll.options) {
let optWrapper = new PollOptionWrapper(i, opt, this.poll.votes_count, opt.votes_count === maxVotes);
let optWrapper = new PollOptionWrapper(i, opt, this.poll.votes_count, this.poll.voters_count, opt.votes_count === maxVotes);
this.options.push(optWrapper);
i++;
}
@ -83,6 +93,7 @@ export class PollComponent implements OnInit {
private checkStatus(accounts: AccountInfo[]): void {
this.pollLocked = false;
this.errorOccuredWhenRetrievingPoll = false;
var newSelectedAccount = accounts.find(x => x.isSelected);
const accountChanged = this.selectedAccount.id !== newSelectedAccount.id;
@ -92,7 +103,7 @@ export class PollComponent implements OnInit {
let statusWrapper = new StatusWrapper(this.statusWrapper.status, this.statusWrapper.provider, this.statusWrapper.applyCw, this.statusWrapper.hide);
this.pollPerAccountId[newSelectedAccount.id] = this.toolsService.getStatusUsableByAccount(newSelectedAccount, statusWrapper)
.then((status: Status) => {
if(!status || !(status.poll)) return null;
if (!status || !(status.poll)) return null;
return this.mastodonService.getPoll(newSelectedAccount, status.poll.id);
})
.then((poll: Poll) => {
@ -100,7 +111,9 @@ export class PollComponent implements OnInit {
return poll;
})
.catch(err => {
this.notificationService.notifyHttpError(err, newSelectedAccount);
//this.notificationService.notifyHttpError(err, newSelectedAccount);
this.errorOccuredWhenRetrievingPoll = true;
this.pollPerAccountId[newSelectedAccount.id] = null;
return null;
});
} else if (this.statusWrapper.status.visibility !== 'public' && this.statusWrapper.status.visibility !== 'unlisted' && this.statusWrapper.provider.id !== newSelectedAccount.id) {
@ -115,8 +128,9 @@ export class PollComponent implements OnInit {
this.selectedAccount = newSelectedAccount;
}
vote(): boolean {
if (this.errorOccuredWhenRetrievingPoll) return false;
const selectedAccount = this.selectedAccount;
const pollPromise = this.pollPerAccountId[selectedAccount.id];
@ -140,6 +154,8 @@ export class PollComponent implements OnInit {
}
refresh(): boolean {
if (this.errorOccuredWhenRetrievingPoll) return false;
this.setStatsAtZero();
const selectedAccount = this.selectedAccount;
@ -175,14 +191,19 @@ export class PollComponent implements OnInit {
}
class PollOptionWrapper implements PollOption {
constructor(index: number, option: PollOption, totalVotes: number, isMax: boolean) {
constructor(index: number, option: PollOption, totalVotes: number, totalVoters: number, isMax: boolean) {
let votesDivider = totalVotes;
if(totalVoters && totalVoters > 0){
votesDivider = totalVoters;
}
this.id = index;
this.title = option.title;
this.votes_count = option.votes_count;
if (totalVotes === 0) {
this.percentage = '0';
} else {
this.percentage = ((this.votes_count / totalVotes) * 100).toFixed(0);
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
}
this.isMax = isMax;
}

View File

@ -0,0 +1,6 @@
<div class="translation translation__button-display" *ngIf="isTranslationAvailable && showTranslationButton">
<a href class="translation__link translation__button-display__link" (click)="translate()">Translate</a>
</div>
<div class="translation translation__display" *ngIf="isTranslationAvailable && !showTranslationButton">
<span class="translation__by">Translated by {{translatedBy}}</span> <a href (click)="revertTranslation()" class="translation__link translation__display__link">revert</a>
</div>

View File

@ -0,0 +1,44 @@
@import "variables";
@import "commons";
$translation-color: #656b8f;
$translation-color-hover: #9fa5ca;
.translation {
margin: 0 10px 0 $avatar-column-space;
color: $translation-color;
font-size: 12px;
&__button-display {
text-align: center;
&__link {
display: block;
padding: 5px 5px 0 5px;
}
}
&__display {
display: flex;
justify-content: space-between;
&__link {
padding: 5px 0 0 0;
}
}
&__link {
color: $translation-color;
transition: all .2s;
&:hover {
text-decoration: none;
color: $translation-color-hover;
}
}
&__by {
display: block;
text-align: left;
padding: 5px 0 0 0;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StatusTranslateComponent } from './status-translate.component';
xdescribe('StatusTranslateComponent', () => {
let component: StatusTranslateComponent;
let fixture: ComponentFixture<StatusTranslateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StatusTranslateComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatusTranslateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,118 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { StatusWrapper } from '../../../../models/common.model';
import { ILanguage } from '../../../../states/settings.state';
import { LanguageService } from '../../../../services/language.service';
import { InstancesInfoService } from '../../../../services/instances-info.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Translation } from '../../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../../services/notification.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-status-translate',
templateUrl: './status-translate.component.html',
styleUrls: ['./status-translate.component.scss']
})
export class StatusTranslateComponent implements OnInit, OnDestroy {
private languageSub: Subscription;
private languagesSub: Subscription;
private loadedTranslation: Translation;
selectedLanguage: ILanguage;
configuredLanguages: ILanguage[] = [];
isTranslationAvailable: boolean;
showTranslationButton: boolean = true;
translatedBy: string;
@Input() status: StatusWrapper;
@Output() translation = new EventEmitter<Translation>();
constructor(
private readonly mastodonWrapperService: MastodonWrapperService,
private readonly languageService: LanguageService,
private readonly instancesInfoService: InstancesInfoService,
private readonly notificationService: NotificationService
) { }
ngOnInit() {
this.languageSub = this.languageService.selectedLanguageChanged.subscribe(l => {
if (l) {
this.selectedLanguage = l;
this.analyseAvailability();
}
});
this.languagesSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
if (l) {
this.configuredLanguages = l;
this.analyseAvailability();
}
});
}
ngOnDestroy(): void {
if (this.languageSub) this.languageSub.unsubscribe();
if (this.languagesSub) this.languagesSub.unsubscribe();
}
private analyseAvailability() {
this.instancesInfoService.getTranslationAvailability(this.status.provider)
.then(canTranslate => {
if (canTranslate
&& !this.status.isRemote
&& this.status.status.language
&& this.configuredLanguages.length > 0
&& this.configuredLanguages.findIndex(x => x.iso639 === this.status.status.language) === -1) {
this.isTranslationAvailable = true;
}
else {
this.isTranslationAvailable = false;
}
})
.catch(err => {
console.error(err);
this.isTranslationAvailable = false;
});
}
translate(): boolean {
if(this.loadedTranslation){
this.translation.next(this.loadedTranslation);
this.showTranslationButton = false;
return false;
}
this.mastodonWrapperService.translate(this.status.provider, this.status.status.id, this.selectedLanguage.iso639)
.then(x => {
this.loadedTranslation = x;
this.translation.next(x);
this.translatedBy = x.provider;
this.showTranslationButton = false;
})
.catch((err: HttpErrorResponse) => {
console.error(err);
this.notificationService.notifyHttpError(err, this.status.provider);
});
return false;
}
revertTranslation(): boolean {
let revertTranslate: Translation;
revertTranslate = {
content: this.status.status.content,
language: this.loadedTranslation.detected_source_language,
detected_source_language: this.loadedTranslation.language,
provider: this.loadedTranslation.provider,
spoiler_text: this.status.status.spoiler_text
};
this.translation.next(revertTranslate);
this.showTranslationButton = true;
return false;
}
}

View File

@ -2,8 +2,7 @@
<div class="reblog" *ngIf="reblog">
<a class="reblog__profile-link" href title="{{ status.account.acct }}"
(click)="openAccount(status.account)"
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img
*ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar }}" /></a> boosted
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ getAvatar(status.account) | ensureHttps }}" /></a> boosted
</div>
<div *ngIf="statusWrapper.status.pinned && !notificationType" class="pinned">
<div class="notification--icon">
@ -35,6 +34,17 @@
boosted your status
</div>
</div>
<div *ngIf="notificationType === 'update'">
<div class="notification--icon">
<fa-icon class="update" [icon]="faEdit"></fa-icon>
</div>
<div class="notification--label">
<a href class="notification--link" title="{{ notificationAccount.acct }}"
(click)="openAccount(notificationAccount)"
(auxclick)="openUrl(notificationAccount.url)" innerHTML="{{ notificationAccount | accountEmoji }}"></a>
edited the status you boosted
</div>
</div>
<div *ngIf="notificationType === 'poll'">
<div class="notification--icon">
<fa-icon class="boost" [icon]="faList"></fa-icon>
@ -50,10 +60,9 @@
<div [ngClass]="{'notification--status': notificationAccount }">
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
(click)="openAccount(displayedStatus.account)" (auxclick)="openUrl(displayedStatus.account.url)">
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar"
src="{{ displayedStatus.account.avatar }}" />
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ getAvatar(displayedStatus.account) | ensureHttps }}" />
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ notificationAccount.avatar }}" />
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ getAvatar(notificationAccount) | ensureHttps }}" />
<span class="status__name">
<span class="status__name--displayname"
innerHTML="{{displayedStatus.account | accountEmoji}}"></span><span
@ -87,6 +96,9 @@
<div class="status__labels--label status__labels--remote" title="this status isn't federated with this instance" *ngIf="isRemote">
remote
</div>
<div class="status__labels--label status__labels--edited" title="this status was edited" *ngIf="statusWrapper.status.edited_at">
edited
</div>
</div>
@ -97,10 +109,17 @@
<span class="status__content-warning--title">sensitive content</span>
<span innerHTML="{{ contentWarningText }}"></span>
</a>
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
<div class="status__content-warning__closed" *ngIf="!isContentWarned && contentWarningText" title="content warning">
<span innerHTML="{{ contentWarningText }}"></span>
</div>
<app-databinded-text #databindedtext class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
(textSelected)="textSelected()"></app-databinded-text>
<app-status-translate [status]="displayedStatusWrapper" (translation)="onTranslation($event)"></app-status-translate>
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll"
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>

View File

@ -105,6 +105,17 @@
background-color: rgb(33, 69, 136);
background-color: rgb(38, 77, 148);
}
&--edited {
background-color: rgb(167, 0, 153);
background-color: rgb(0, 128, 167);
background-color: rgb(65, 65, 71);
background-color: rgb(144, 184, 0);
background-color: rgb(82, 105, 0);
background-color: rgb(95, 95, 95);
// color: black;
}
}
&__name {
display: inline-block;
@ -150,7 +161,8 @@
min-height: 80px;
display: block;
margin: 0 10px 0 $avatar-column-space;
padding: 3px 5px 12px 5px;
padding: 3px 5px 14px 5px;
overflow-wrap: break-word;
text-decoration: none;
text-align: center;
@ -160,6 +172,26 @@
border: 3px solid $status-secondary-color;
color: whitesmoke;
&__closed {
//margin: 0 5px 0 $avatar-column-space;
margin: 0 5px 0 calc(#{$avatar-column-space} - 1px);
padding: 3px 5px 3px 5px;
margin-bottom: 5px;
overflow-wrap: break-word;
font-size: 12px;
border-radius: 4px;
// color: #6d8fd3;
// color: #7282a1;
// color: #838da1;
color: #919bb1;
// background-color: #273149;
// background-color: #1f273a;
background-color: #171d2b;
}
&--title {
color: $content-warning-font-color;
font-size: 11px;
@ -246,6 +278,10 @@
color: $boost-color;
}
.update {
color: $update-color;
}
.favorite {
color: $favorite-color;
}
@ -260,4 +296,4 @@
&__label{
color: $status-secondary-color;
}
}
}

View File

@ -1,13 +1,16 @@
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core";
import { faStar, faRetweet, faList, faThumbtack } from "@fortawesome/free-solid-svg-icons";
import { faStar, faRetweet, faList, faThumbtack, faEdit } from "@fortawesome/free-solid-svg-icons";
import { Subscription } from "rxjs";
import { Status, Account } from "../../../services/models/mastodon.interfaces";
import { Status, Account, Translation } from "../../../services/models/mastodon.interfaces";
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
import { ActionBarComponent } from "./action-bar/action-bar.component";
import { StatusWrapper } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { ContentWarningPolicyEnum } from '../../../states/settings.state';
import { stat } from 'fs';
import { StatusesStateService, StatusState } from "../../../services/statuses-state.service";
import { DatabindedTextComponent } from "./databinded-text/databinded-text.component";
import { SettingsService } from "../../../services/settings.service";
@Component({
selector: "app-status",
@ -21,6 +24,7 @@ export class StatusComponent implements OnInit {
faRetweet = faRetweet;
faList = faList;
faThumbtack = faThumbtack;
faEdit = faEdit;
displayedStatus: Status;
displayedStatusWrapper: StatusWrapper;
@ -41,6 +45,8 @@ export class StatusComponent implements OnInit {
isSelected: boolean;
isRemote: boolean;
private freezeAvatarEnabled: boolean;
hideStatus: boolean = false;
@Output() browseAccountEvent = new EventEmitter<string>();
@ -50,12 +56,16 @@ export class StatusComponent implements OnInit {
@Input() isThreadDisplay: boolean;
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll';
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll' | 'update';
@Input() notificationAccount: Account;
@Input() context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
private _statusWrapper: StatusWrapper;
status: Status;
private statusesStateServiceSub: Subscription;
@Input('statusWrapper')
set statusWrapper(value: StatusWrapper) {
this._statusWrapper = value;
@ -88,7 +98,10 @@ export class StatusComponent implements OnInit {
// const instanceUrl = 'https://' + this.status.uri.split('https://')[1].split('/')[0];
// this.statusAccountName = this.emojiConverter.applyEmojis(this.displayedStatus.account.emojis, this.displayedStatus.account.display_name, EmojiTypeEnum.small);
this.statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, this.displayedStatus.content, EmojiTypeEnum.medium);
let statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, this.displayedStatus.content, EmojiTypeEnum.medium);
this.statusContent = this.ensureMentionAreDisplayed(statusContent);
this.validateFilteringStatus();
}
get statusWrapper(): StatusWrapper {
return this._statusWrapper;
@ -96,58 +109,91 @@ export class StatusComponent implements OnInit {
constructor(
public elem: ElementRef,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService,
private readonly settingsService: SettingsService,
private readonly statusesStateService: StatusesStateService) { }
ngOnInit() {
this.statusesStateServiceSub = this.statusesStateService.stateNotification.subscribe(notification => {
if (this._statusWrapper.status.url === notification.statusId && notification.isEdited) {
this.statusWrapper = notification.editedStatus;
}
});
this.freezeAvatarEnabled = this.settingsService.getSettings().enableFreezeAvatar;
}
// private checkContentWarning(status: Status) {
// let cwPolicy = this.toolsService.getSettings().contentWarningPolicy;
ngOnDestroy() {
if (this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
}
// let splittedContent = [];
// if ((cwPolicy.policy === ContentWarningPolicyEnum.HideAll && cwPolicy.addCwOnContent.length > 0)
// || (cwPolicy.policy === ContentWarningPolicyEnum.AddOnAllContent && cwPolicy.removeCwOnContent.length > 0)
// || (cwPolicy.hideCompletlyContent && cwPolicy.hideCompletlyContent.length > 0)) {
// let parser = new DOMParser();
// let dom = parser.parseFromString((status.content + ' ' + status.spoiler_text).replace("<br/>", " ").replace("<br>", " ").replace(/\n/g, ' '), 'text/html')
// let contentToParse = dom.body.textContent;
// splittedContent = contentToParse.toLowerCase().split(' ');
// }
private validateFilteringStatus(){
const filterStatus = this.displayedStatus.filtered;
// if (cwPolicy.policy === ContentWarningPolicyEnum.None && (status.sensitive || status.spoiler_text)) {
// this.setContentWarning(status);
// } else if (cwPolicy.policy === ContentWarningPolicyEnum.HideAll) {
// let detected = cwPolicy.addCwOnContent.filter(x => splittedContent.find(y => y == x || y == `#${x}`));
// if (!detected || detected.length === 0) {
// this.status.sensitive = false;
// } else {
// if (!status.spoiler_text) {
// status.spoiler_text = detected.join(' ');
// }
// this.setContentWarning(status);
// }
// } else if (cwPolicy.policy === ContentWarningPolicyEnum.AddOnAllContent) {
// let detected = cwPolicy.removeCwOnContent.filter(x => splittedContent.find(y => y == x || y == `#${x}`));
if(!filterStatus || filterStatus.length === 0) return;
// if (detected && detected.length > 0) {
// this.status.sensitive = false;
// } else {
// this.setContentWarning(status);
// }
// }
// if(!this.context){
// console.warn('this.context not found');
// console.warn(this.context);
// }
// if (cwPolicy.hideCompletlyContent && cwPolicy.hideCompletlyContent.length > 0) {
// let detected = cwPolicy.hideCompletlyContent.filter(x => splittedContent.find(y => y == x || y == `#${x}`));
// if (detected && detected.length > 0) {
// this.hideStatus = true;
// }
// }
// }
for (let filter of filterStatus) {
if(this.context && filter.filter.context && filter.filter.context.length > 0){
if(!filter.filter.context.includes(this.context)) continue;
}
if(filter.filter.filter_action === 'warn'){
this.isContentWarned = true;
let filterTxt = `FILTERED:`;
for(let w of filter.keyword_matches){
filterTxt += ` ${w}`;
}
this.contentWarningText = filterTxt;
} else if (filter.filter.filter_action === 'hide'){
this.hideStatus = true;
}
}
}
getAvatar(acc: Account): string {
if(this.freezeAvatarEnabled){
return acc.avatar_static;
} else {
return acc.avatar;
}
}
private ensureMentionAreDisplayed(data: string): string {
const mentions = this.displayedStatus.mentions;
if (!mentions || mentions.length === 0) return data;
let textMentions = '';
for (const m of mentions) {
if (!data.includes(m.url)) {
textMentions += `<span class="h-card"><a class="u-url mention" data-user="${m.id}" href="${m.url}" rel="ugc">@<span>${m.username}</span></a></span> `
}
}
if (textMentions !== '') {
data = textMentions + data;
}
return data;
}
private setContentWarning(status: StatusWrapper) {
this.hideStatus = status.hide;
this.isContentWarned = status.applyCw;
this.contentWarningText = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, status.status.spoiler_text, EmojiTypeEnum.medium);
let spoiler = this.htmlEncode(status.status.spoiler_text);
this.contentWarningText = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, spoiler, EmojiTypeEnum.medium);
}
private htmlEncode(str: string): string {
var encodedStr = str.replace(/[\u00A0-\u9999<>\&]/gim, function (i) {
return '&#' + i.charCodeAt(0) + ';';
});
return encodedStr;
}
removeContentWarning(): boolean {
@ -159,6 +205,31 @@ export class StatusComponent implements OnInit {
changeCw(cwIsActive: boolean) {
this.isContentWarned = cwIsActive;
}
@ViewChild('databindedtext') public databindedText: DatabindedTextComponent;
onTranslation(translation: Translation) {
let statusContent = translation.content;
// clean up a bit some issues (not reliable)
while (statusContent.includes('<span>@')) {
statusContent = statusContent.replace('<span>@', '@<span>');
}
while (statusContent.includes('h<span class="invisible">')){
statusContent = statusContent.replace('h<span class="invisible">', '<span class="invisible">h');
}
while (statusContent.includes('<span>#')){
statusContent = statusContent.replace('<span>#', '#<span>');
}
statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, statusContent, EmojiTypeEnum.medium);
this.statusContent = this.ensureMentionAreDisplayed(statusContent);
setTimeout(x => {
this.databindedText.processEventBindings();
}, 500);
}
private checkLabels(status: Status) {
//since API is limited with federated status...

View File

@ -10,13 +10,14 @@ import { NotificationWrapper } from '../../floating-column/manage-account/notifi
import { AccountInfo } from '../../../states/accounts.state';
import { NotificationService } from '../../../services/notification.service';
import { StreamingService, StatusUpdate, EventEnum } from '../../../services/streaming.service';
import { BrowseBase } from '../../common/browse-base';
@Component({
selector: 'app-stream-notifications',
templateUrl: './stream-notifications.component.html',
styleUrls: ['./stream-notifications.component.scss']
})
export class StreamNotificationsComponent implements OnInit, OnDestroy {
export class StreamNotificationsComponent extends BrowseBase {
displayingNotifications = true;
displayingMentions = false;
@ -26,10 +27,6 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
@Input() streamElement: StreamElement;
@Input() goToTop: Observable<void>;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ViewChild('notificationstream') public notificationstream: ElementRef;
@ViewChild('mentionstream') public mentionstream: ElementRef;
@ -53,7 +50,9 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService,
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService) {
super();
}
ngOnInit() {
this.goToTopSubscription = this.goToTop.subscribe(() => {
@ -123,18 +122,21 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
loadNotifications(): any {
this.account = this.toolsService.getAccountById(this.streamElement.accountId);
this.mentionsSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
this.mentionsSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
this.loadMentions(userNotifications);
});
this.mastodonService.getNotifications(this.account, null, null, null, 10)
this.mastodonService.getNotifications(this.account, [], null, null, 10)
.then((notifications: Notification[]) => {
this.isNotificationsLoading = false;
this.notifications = notifications.map(x => {
let wrappedNotification= notifications.map(x => {
let cwPolicy = this.toolsService.checkContentWarning(x.status);
return new NotificationWrapper(x, this.account, cwPolicy.applyCw, cwPolicy.hide);
});
this.notifications = wrappedNotification.filter(x => x.type !== 'mention' || (x.type === 'mention' && x.status.status !== null));
this.lastNotificationId = this.notifications[this.notifications.length - 1].notification.id;
})
.catch(err => {
@ -195,13 +197,14 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
}
}
private scrolledErrorOccured = false;
notificationsScrolledToBottom(): any {
if (this.isNotificationsLoading || this.notificationsMaxReached || this.notifications.length === 0)
if (this.isNotificationsLoading || this.notificationsMaxReached || this.notifications.length === 0 || this.scrolledErrorOccured)
return;
this.isNotificationsLoading = true;
this.mastodonService.getNotifications(this.account, null, this.lastNotificationId)
this.mastodonService.getNotifications(this.account, ['update'], this.lastNotificationId)
.then((result: Notification[]) => {
if (result.length === 0) {
this.notificationsMaxReached = true;
@ -217,6 +220,11 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
this.lastNotificationId = result[result.length - 1].id;
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
@ -225,12 +233,12 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
}
mentionsScrolledToBottom(): any {
if (this.isMentionsLoading || this.mentionsMaxReached || this.mentions.length === 0)
if (this.isMentionsLoading || this.mentionsMaxReached || this.mentions.length === 0 || this.scrolledErrorOccured)
return;
this.isMentionsLoading = true;
this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll'], this.lastMentionId)
this.mastodonService.getNotifications(this.account, ['follow', 'favourite', 'reblog', 'poll', 'follow_request', 'move', 'update'], this.lastMentionId)
.then((result: Notification[]) => {
if (result.length === 0) {
this.mentionsMaxReached = true;
@ -246,6 +254,11 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
this.lastMentionId = result[result.length - 1].id;
})
.catch(err => {
this.scrolledErrorOccured = true;
setTimeout(() => {
this.scrolledErrorOccured = false;
}, 5000);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
@ -265,16 +278,4 @@ export class StreamNotificationsComponent implements OnInit, OnDestroy {
}, 0);
return false;
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -1,9 +1,5 @@
<div class="overlay">
<div class="overlay__header">
<a href class="overlay__button overlay-close" title="close" (click)="close()">
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
</a>
<a href class="overlay__button overlay-previous"
[ngClass]="{'overlay__button--focus': hasPreviousElements }" title="previous" (click)="previous()">
<fa-icon class="overlay-previous__icon" [icon]="faAngleLeft"></fa-icon>
@ -12,13 +8,17 @@
title="refresh" (click)="refresh()">
<fa-icon class="overlay-refresh__icon" [icon]="faRedoAlt"></fa-icon>
</a>
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
title="next" (click)="next()">
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
</a>
<a href title="return to top" class="overlay-gototop" (click)="goToTop()">
</a>
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
title="next" (click)="next()">
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
<a href class="overlay__button overlay-close" title="close" (click)="close()">
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
</a>
</div>
@ -31,7 +31,16 @@
class="overlay__content"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-user-profile>
(browseThreadEvent)="browseThread($event)"
(browseFollowsEvent)="browseFollows($event)"
(browseFollowersEvent)="browseFollowers($event)"></app-user-profile>
<app-user-follows *ngIf="e.type === 'follows' || e.type === 'followers'"
[currentAccount]="e.account"
[type]="e.type"
[refreshEventEmitter]="e.refreshEventEmitter"
[goToTopEventEmitter]="e.goToTopEventEmitter"
class="overlay__content"
(browseAccountEvent)="browseAccount($event)"></app-user-follows>
<app-hashtag #appHashtag *ngIf="e.type === 'hashtag'"
[hashtagElement]="e.hashtag"
[refreshEventEmitter]="e.refreshEventEmitter"

View File

@ -11,7 +11,9 @@ $header-content-height: 40px;
width: calc(100%);
height: $header-content-height;
background-color: $column-header-background-color;
border-bottom: 1px solid #222736;
border-bottom: 1px solid #222736;
display: flex;
}
&__content-wrapper {
transition: all .2s;
@ -44,11 +46,17 @@ $header-content-height: 40px;
}
&__button {
// outline: 1px dotted orange;
width: 25px;
height: 25px;
width: $header-content-height;
height: $header-content-height;
color: #354060;
transition: all .2s;
margin: 8px 0 0 8px;
&:hover {
color: #536599;
color: #7a8dc7;
@ -68,19 +76,8 @@ $header-content-height: 40px;
&__icon {
position: relative;
left: 7px;
top: -1px
}
}
&-next {
display: block;
float: left;
font-size: 18px;
&__icon {
position: relative;
left: 8px;
top: -1px
left: 17px;
top: 7px
}
}
&-refresh {
@ -90,29 +87,38 @@ $header-content-height: 40px;
&__icon {
position: relative;
left: 5px;
top: 1px
left: 13px;
top: 9px
}
}
&-next {
display: block;
float: left;
font-size: 18px;
&__icon {
position: relative;
left: 13px;
top: 7px
}
}
&-gototop {
position: absolute;
top: 0;
left: 110px;
right: 40px;
// outline: 1px dotted orange;
flex-grow: 1;
display: block;
height: $header-content-height;
}
&-close {
display: block;
float: right;
font-size: 13px;
color: white;
margin-right: 8px;
&__icon {
position: relative;
left: 7px;
top: 1px
left: 15px;
top: 9px
}
}
}

View File

@ -123,7 +123,7 @@ export class StreamOverlayComponent implements OnInit, OnDestroy {
browseAccount(accountName: string): void {
if (!accountName) return;
const newElement = new OverlayBrowsing(null, accountName, null);
const newElement = new OverlayBrowsing('account', null, accountName, null);
this.loadElement(newElement);
}
@ -132,14 +132,28 @@ export class StreamOverlayComponent implements OnInit, OnDestroy {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
const hashTagElement = new StreamElement(StreamTypeEnum.tag, hashtag, selectedAccount.id, hashtag, null, null, selectedAccount.instance);
const newElement = new OverlayBrowsing(hashTagElement, null, null);
const newElement = new OverlayBrowsing('hashtag', hashTagElement, null, null);
this.loadElement(newElement);
}
browseThread(openThread: OpenThreadEvent): any {
if (!openThread) return;
const newElement = new OverlayBrowsing(null, null, openThread);
const newElement = new OverlayBrowsing('thread', null, null, openThread);
this.loadElement(newElement);
}
browseFollows(accountName: string): void {
if (!accountName) return;
const newElement = new OverlayBrowsing('follows', null, accountName, null);
this.loadElement(newElement);
}
browseFollowers(accountName: string): void {
if (!accountName) return;
const newElement = new OverlayBrowsing('followers', null, accountName, null);
this.loadElement(newElement);
}
@ -167,19 +181,10 @@ class OverlayBrowsing {
goToTopEventEmitter = new EventEmitter();
constructor(
public readonly type: 'hashtag' | 'account' | 'thread' | 'follows' | 'followers',
public readonly hashtag: StreamElement,
public readonly account: string,
public readonly thread: OpenThreadEvent) {
if (hashtag) {
this.type = 'hashtag';
} else if (account) {
this.type = 'account';
} else if (thread) {
this.type = 'thread';
} else {
throw Error('NotImplemented');
}
}
show(): any {
@ -198,5 +203,4 @@ class OverlayBrowsing {
}
isVisible: boolean;
type: 'hashtag' | 'account' | 'thread';
}

View File

@ -5,20 +5,23 @@
</a>
</div>
<div class="stream-toots__new-notification"
<div class="stream-toots__new-notification"
[class.stream-toots__new-notification--display]="bufferStream && bufferStream.length > 0 && !streamPositionnedAtTop"></div>
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
<div *ngIf="timelineLoadingMode === 3 && bufferStream && bufferStream.length > 0">
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ bufferStream.length }} new item<span *ngIf="bufferStream.length > 1">s</span></a>
<div *ngIf="timelineLoadingMode === 3 && bufferStream && numNewItems > 0">
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ numNewItems }} new item<span *ngIf="numNewItems > 1">s</span></a>
</div>
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
<app-status
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
<app-status
[statusWrapper]="statusWrapper"
[isThreadDisplay]="isThread"
[context]="context"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div>

View File

@ -1,49 +1,29 @@
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, EventEmitter, Output, ViewChildren, QueryList } from '@angular/core';
import { Component, Input } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement } from '../../../states/streams.state';
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
import { AccountInfo } from '../../../states/accounts.state';
import { StreamingService, EventEnum, StreamingWrapper, StatusUpdate } from '../../../services/streaming.service';
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
import { Status } from '../../../services/models/mastodon.interfaces';
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
import { NotificationService } from '../../../services/notification.service';
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
import { ToolsService } from '../../../services/tools.service';
import { StatusWrapper } from '../../../models/common.model';
import { TimeLineModeEnum } from '../../../states/settings.state';
import { TimelineBase } from '../../common/timeline-base';
import { SettingsService } from '../../../services/settings.service';
@Component({
selector: 'app-stream-statuses',
templateUrl: './stream-statuses.component.html',
styleUrls: ['./stream-statuses.component.scss']
})
export class StreamStatusesComponent implements OnInit, OnDestroy {
isLoading = true;
private lastInfinityFetchReturnedNothing = false;
isThread = false;
displayError: string;
hasContentWarnings = false;
export class StreamStatusesComponent extends TimelineBase {
protected _streamElement: StreamElement;
timelineLoadingMode: TimeLineModeEnum;
private _streamElement: StreamElement;
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = [];
bufferStream: Status[] = [];
private bufferWasCleared: boolean;
streamPositionnedAtTop: boolean = true;
private isProcessingInfiniteScroll: boolean;
private hideBoosts: boolean;
private hideReplies: boolean;
private hideBots: boolean;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
@Input()
set streamElement(streamElement: StreamElement) {
@ -54,15 +34,13 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.hideReplies = streamElement.hideReplies;
this.load(this._streamElement);
this.setContext(this._streamElement);
}
get streamElement(): StreamElement {
return this._streamElement;
}
@Input() goToTop: Observable<void>;
@Input() userLocked = true;
private goToTopSubscription: Subscription;
private streamsSubscription: Subscription;
private hideAccountSubscription: Subscription;
@ -70,17 +48,19 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private streams$: Observable<StreamElement[]>;
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonWrapperService) {
protected readonly settingsService: SettingsService,
protected readonly store: Store,
protected readonly toolsService: ToolsService,
protected readonly notificationService: NotificationService,
protected readonly streamingService: StreamingService,
protected readonly mastodonService: MastodonWrapperService) {
super(toolsService, notificationService, mastodonService);
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
}
ngOnInit() {
let settings = this.toolsService.getSettings();
let settings = this.settingsService.getSettings();
this.timelineLoadingMode = settings.timelineMode;
this.goToTopSubscription = this.goToTop.subscribe(() => {
@ -125,6 +105,8 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
});
}
});
this.numNewItems = 0;
}
ngOnDestroy() {
@ -134,11 +116,29 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
if (this.deleteStatusSubscription) this.deleteStatusSubscription.unsubscribe();
}
private setContext(streamElement: StreamElement) {
switch(streamElement.type){
case StreamTypeEnum.global:
case StreamTypeEnum.local:
case StreamTypeEnum.tag:
this.context = 'public';
break;
case StreamTypeEnum.personnal:
case StreamTypeEnum.list:
this.context = 'home';
break;
case StreamTypeEnum.activity:
case StreamTypeEnum.directmessages:
this.context = 'notifications';
break;
}
}
refresh(): any {
this.load(this._streamElement);
}
private load(streamElement: StreamElement) {
protected load(streamElement: StreamElement) {
this.resetStream();
if (this.userLocked) {
@ -157,6 +157,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private resetStream() {
this.statuses.length = 0;
this.bufferStream.length = 0;
this.numNewItems = 0;
if (this.websocketStreaming) this.websocketStreaming.dispose();
}
@ -178,6 +179,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.statuses.unshift(wrapper);
} else {
this.bufferStream.push(update.status);
this.numNewItems++;
}
}
} else if (update.type === EventEnum.delete) {
@ -190,54 +192,17 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
});
}
@ViewChild('statusstream') public statustream: ElementRef;
private applyGoToTop(): boolean {
// this.loadBuffer();
protected statusProcessOnGoToTop(){
if (this.statuses.length > 2 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
}
const stream = this.statustream.nativeElement as HTMLElement;
setTimeout(() => {
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
}, 0);
return false;
}
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
const atTop = element.scrollTop === 0;
this.streamPositionnedAtTop = false;
if (atBottom && !this.isProcessingInfiniteScroll) {
this.scrolledToBottom();
} else if (atTop) {
this.scrolledToTop();
}
}
browseAccount(accountName: string): void {
this.browseAccountEvent.next(accountName);
}
browseHashtag(hashtag: string): void {
this.browseHashtagEvent.next(hashtag);
}
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
textSelected(): void {
console.warn(`status comp: textSelected`); //TODO
}
private scrolledToTop() {
protected scrolledToTop() {
this.streamPositionnedAtTop = true;
if (this.timelineLoadingMode !== TimeLineModeEnum.SlowMode) {
@ -262,41 +227,38 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
}
this.bufferStream.length = 0;
this.numNewItems = 0;
return false;
}
private scrolledToBottom() {
if (this.isLoading || this.lastInfinityFetchReturnedNothing) return;
this.isLoading = true;
this.isProcessingInfiniteScroll = true;
protected getNextStatuses(): Promise<Status[]> {
const lastStatus = this.statuses[this.statuses.length - 1];
this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.listId)
.then((status: Status[]) => {
for (const s of status) {
if (this.isFiltered(s)) {
continue;
}
let cwPolicy = this.toolsService.checkContentWarning(s);
const wrapper = new StatusWrapper(cwPolicy.status, this.account, cwPolicy.applyCw, cwPolicy.hide);
this.statuses.push(wrapper);
}
if (!status || status.length === 0) {
this.lastInfinityFetchReturnedNothing = true;
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
this.isProcessingInfiniteScroll = false;
return this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.listId)
.then((status: Status[]) =>{
return status.filter(x => !this.isFiltered(x));
});
}
private isFiltered(status: Status): boolean {
if (this.streamElement.hideBoosts) {
if (status.reblog) {
return true;
}
}
if (this.streamElement.hideBots) {
if (status.account.bot) {
return true;
}
}
if (this.streamElement.hideReplies) {
if (status.in_reply_to_account_id && status.account.id !== status.in_reply_to_account_id) {
return true;
}
}
return false;
}
private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
@ -306,7 +268,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
focus(): boolean {
setTimeout(() => {
var element = this.statustream.nativeElement as HTMLElement;
element.focus({preventScroll:true});
element.focus({ preventScroll: true });
}, 0);
return false;
}
@ -333,7 +295,8 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private checkAndCleanUpStream(): void {
if (this.streamPositionnedAtTop && this.statuses.length > 3 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
this.lastInfinityFetchReturnedNothing = false;
this.maxReached = false;
this.lastCallReachedMax = false;
}
if (this.bufferStream.length > 3 * this.streamingService.nbStatusPerIteration) {
@ -341,24 +304,5 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.bufferStream.length = 2 * this.streamingService.nbStatusPerIteration;
}
}
private isFiltered(status: Status): boolean {
if (this.streamElement.hideBoosts) {
if (status.reblog) {
return true;
}
}
if (this.streamElement.hideBots) {
if (status.account.bot) {
return true;
}
}
if (this.streamElement.hideReplies) {
if (status.in_reply_to_account_id && status.account.id !== status.in_reply_to_account_id) {
return true;
}
}
return false;
}
}

View File

@ -7,11 +7,13 @@
<!-- <div> -->
<div class="stream-column__stream-header">
<a class="stream-column__stream-selector" href title="return to top" (click)="goToTop()">
<img *ngIf="timelineHeader === 3 || timelineHeader === 4" class="stream-column__stream-selector--avatar" src="{{avatar}}" />
<img *ngIf="timelineHeader === 3 || timelineHeader === 4 || timelineHeader === 6" class="stream-column__stream-selector--avatar" src="{{avatar}}" />
<fa-icon class="stream-column__stream-selector--icon" [icon]="columnFaIcon"></fa-icon>
<span class="stream-column__stream-selector--text">
<h1 class="stream-column__stream-selector--title" [class.stream-column__stream-selector--title--only]="timelineHeader === 4 || timelineHeader === 5">{{ streamElement.name.toUpperCase() }}</h1>
<span class="stream-column__stream-selector--subtitle" *ngIf="streamElement.instance && timelineHeader !== 4 && timelineHeader !== 5"><span *ngIf="timelineHeader === 2">{{account.username}}@</span>{{ streamElement.instance.toLowerCase() }}</span>
<span class="stream-column__stream-selector--subtitle" *ngIf="streamElement.instance && timelineHeader !== 4 && timelineHeader !== 5">
<span *ngIf="timelineHeader === 2 || timelineHeader === 6">{{account.username}}@</span>{{ streamElement.instance.toLowerCase() }}
</span>
</span>
</a>
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">

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