Compare commits

...

163 Commits

Author SHA1 Message Date
7e0cfee8f1 Merge branch 'main' of https://github.com/JohnXLivingston/peertube-plugin-livechat 2024-09-05 22:17:17 -04:00
John Livingston
2f78b901e3
v11.0.1:
* Fix "send message" button that was sending the message twice.
* Bump v11.0.1
2024-09-03 15:28:57 +02:00
John Livingston
575703a7e5
Bump v11.0.0 + npm audit fix. 2024-09-03 14:42:14 +02:00
John Livingston
8e0f239993
Fix typo. 2024-09-03 14:16:03 +02:00
John Livingston
8a1948520d
Fix regression for muc sidebar (related to Converse upstream). 2024-09-02 17:07:58 +02:00
John Livingston
0fe0ebfb3e
Fix css regression (related to Converse upstream). 2024-09-02 14:52:07 +02:00
John Livingston
9ee4476f4d
Fix: improved minimum chat width. 2024-09-02 14:39:26 +02:00
John Livingston
0e98cbaba5
ConverseJS upstream update:
* update ConverseJS version
* remove concord theme from settings (and migrate to peertube)
* added cyberpunk theme
* fixed settings localization
2024-09-02 12:11:21 +02:00
John Livingston
22dc4db61b
Merge pull request #511 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-09-02 11:53:31 +02:00
Victor Hampel
42147148ea
Translated using Weblate (German)
Currently translated at 100.0% (880 of 880 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-09-02 11:47:09 +02:00
ButterflyOfFire
87e8f9fd39
Translated using Weblate (Arabic)
Currently translated at 13.2% (117 of 880 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/ar/
2024-09-02 11:47:09 +02:00
John Livingston
033a86b3a6
Changelog. 2024-09-02 11:47:01 +02:00
John Livingston
8394e7222d
Update Typescript version. 2024-09-02 11:46:10 +02:00
John Livingston
d8fc90dd1f
Merge pull request #512 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-09-02 11:44:31 +02:00
Besnik Bleta
04db24b6af
Translated using Weblate (Albanian)
Currently translated at 65.8% (195 of 296 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/sq/
2024-08-30 22:08:35 +02:00
josé m
b0d65add1f
Translated using Weblate (Galician)
Currently translated at 10.1% (30 of 296 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/gl/
2024-08-30 22:08:35 +02:00
ButterflyOfFire
6b69f0bf46
Translated using Weblate (Arabic)
Currently translated at 31.7% (94 of 296 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/ar/
2024-08-30 22:08:34 +02:00
T.S
6373af32ba
Translated using Weblate (Japanese)
Currently translated at 100.0% (296 of 296 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/ja/
2024-08-30 22:08:34 +02:00
Victor Hampel
d87cdbb1ff
Translated using Weblate (German)
Currently translated at 100.0% (296 of 296 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-08-30 22:08:34 +02:00
John Livingston
5d87121631
npm run doc:translate 2024-08-30 16:24:35 +02:00
John Livingston
c205acbb17
Improved documentation accessibility (#118):
* Adding alts and titles on screenshots. Alts are now descriptive.
2024-08-30 16:21:16 +02:00
John Livingston
5462e3d52f
Documentation: fix titles using livechat_label shortcode
This fix will avoid weird "hahahugoshortcode" anchor and title in the
navigation menu.
2024-08-30 14:19:37 +02:00
John Livingston
ef5bc3cb8a
Fix gitlab CI. 2024-08-30 12:39:34 +02:00
John Livingston
d8da3ca3b8
Improved documentation accessibility (#118):
* borders on focused links.
2024-08-30 12:39:34 +02:00
John Livingston
3d8fbba767
Documentation: improve home links. 2024-08-30 12:39:33 +02:00
John Livingston
8183dc82bb
Documentation: stable language selector order. 2024-08-30 12:39:33 +02:00
John Livingston
a799a9c07e
Improved documentation accessibility (#118):
* underline links in the right footer.
2024-08-30 12:39:33 +02:00
John Livingston
6eeb19607f
Improved documentation accessibility (#118):
* `white-space: normal` for the breadcrumbs, to avoid it to be
  truncated.
2024-08-30 12:39:33 +02:00
John Livingston
56547cc084
Improved documentation accessibility (#118):
* better contrast.
2024-08-30 12:39:33 +02:00
John Livingston
75925b1117
Documentation: migrating from hugo-theme-learn to hugo-theme-relearn 2024-08-30 12:39:27 +02:00
John Livingston
2824bd1e38
Merge pull request #505 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-30 12:38:34 +02:00
T.S
7293f4a6d5
Translated using Weblate (Japanese)
Currently translated at 6.8% (60 of 879 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/ja/
2024-08-30 12:37:54 +02:00
Victor Hampel
f7ddd58a2c
Translated using Weblate (German)
Currently translated at 100.0% (879 of 879 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-08-30 12:37:54 +02:00
John Livingston
4a747d7314
Merge pull request #508 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-08-30 12:37:48 +02:00
John Livingston
f5074934e4
Translated using Weblate (French)
Currently translated at 100.0% (296 of 296 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/fr/
2024-08-29 21:15:47 +02:00
John Livingston
6c0b5e1c19
Translated using Weblate (French)
Currently translated at 96.6% (285 of 295 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/fr/
2024-08-29 14:36:05 +02:00
T.S
1b75f3d504
Translated using Weblate (Japanese)
Currently translated at 100.0% (295 of 295 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/ja/
2024-08-29 14:36:05 +02:00
John Livingston
b673a49af6
Improved accessibility (#118):
* channel configuration: adding title to inputs.
* channel configuration: `aria-hidden="true"` on icons for add and
  remove row buttons.
2024-08-29 12:20:36 +02:00
John Livingston
944bdcebb7
Improved accessibility (#118):
* aria-hidden on the channel avatar in the `p/livechat/configuration`
  page.
2024-08-29 11:28:58 +02:00
John Livingston
c6d012cfb4
Including Converse accessibility fixes. 2024-08-29 11:28:30 +02:00
John Livingston
0732bd1de3
Improved accessibility (#118):
* top chat button accessibility improved (role, aria-hidden for icons,
  tabindex for keyboard navigation, ...)
2024-08-20 17:52:53 +02:00
John Livingston
e65bd5c426
Improved accessibility (#118):
* Adding a role="region" and an aria-label="Chat" on the chat container.
2024-08-20 17:24:18 +02:00
John Livingston
3177c31b08
Improved accessibility (#118):
* Fix autocomplete vs tab key
2024-08-20 16:59:08 +02:00
John Livingston
cee42b4bcc
Improved accessibility (#118):
* adding role="button" or type="button" where missing.
2024-08-20 15:30:45 +02:00
John Livingston
9e252193d4
Fix #504: better prosodyctl documentation 2024-08-19 10:58:49 +02:00
John Livingston
08eb466e27
Merge pull request #502 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-08-19 10:55:09 +02:00
dswhi
0cf5647a89
Translated using Weblate (Chinese (Traditional))
Currently translated at 51.5% (152 of 295 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/zh_Hant/
2024-08-19 10:54:35 +02:00
Victor Hampel
4113259975
Translated using Weblate (German)
Currently translated at 100.0% (295 of 295 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-08-19 10:54:35 +02:00
John Livingston
4bdf40e905
Merge pull request #501 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-19 10:54:27 +02:00
Victor Hampel
a385204256
Translated using Weblate (German)
Currently translated at 100.0% (879 of 879 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-08-15 11:38:20 +02:00
John Livingston
e1d1dd94e6
Update documentation. 2024-08-13 10:49:35 +02:00
John Livingston
940e8c9ac4
Merge pull request #500 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-13 10:47:04 +02:00
Victor Hampel
9a22ab7f18
Translated using Weblate (German)
Currently translated at 100.0% (862 of 862 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-08-13 10:37:16 +02:00
John Livingston
8e99199f29
New option to use and configure Prosody mod_firewall WIP (#97):
* new setting
* new configuration screen for Peertube admins
* include the mod_firewall module
* load mod_firewall if enabled
* sys admin can disable the firewall config editing by creating a
  special file on the disk
* user documentation
2024-08-13 10:35:47 +02:00
John Livingston
481f265a44
Merge pull request #498 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-12 14:19:06 +02:00
John Livingston
6fd8383439
Translated using Weblate (French)
Currently translated at 95.4% (823 of 862 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/fr/
2024-08-12 13:25:35 +02:00
John Livingston
54c31500a3
Fix typo. 2024-08-12 12:09:32 +02:00
John Livingston
e08c413682
Merge pull request #497 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-12 12:08:16 +02:00
John Livingston
73845eb5d4
Translated using Weblate (French)
Currently translated at 94.0% (811 of 862 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/fr/
2024-08-12 12:07:50 +02:00
John Livingston
a47737967a
Merge pull request #496 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-12 12:04:14 +02:00
Victor Hampel
67b89f1aef
Translated using Weblate (German)
Currently translated at 100.0% (862 of 862 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-08-12 12:04:00 +02:00
John Livingston
e1e91c2984
Merge pull request #494 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-08-12 12:03:55 +02:00
Victor Hampel
a813ceb723
Translated using Weblate (German)
Currently translated at 100.0% (285 of 285 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-08-12 11:58:01 +02:00
John Livingston
cd0813fb14
Translated using Weblate (French)
Currently translated at 100.0% (285 of 285 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/fr/
2024-08-12 11:58:01 +02:00
John Livingston
62caa63dc5
ConverseJS: using a version with the occupant actions on the right. 2024-08-12 11:57:31 +02:00
John Livingston
ef1b49f291
Fix: increase chat height on small screens, try to better detect the device viewport size and orientation. 2024-08-07 00:09:55 +02:00
John Livingston
b8db486410
Message search results: display original nickname if has changed. 2024-08-06 17:54:07 +02:00
John Livingston
df75659a05
Merge pull request #492 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-08-06 17:37:43 +02:00
Besnik Bleta
a3555ed3cd
Translated using Weblate (Albanian)
Currently translated at 68.3% (194 of 284 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/sq/
2024-08-06 17:35:19 +02:00
John Livingston
fd7d24c121
Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/fr/
2024-08-06 17:35:19 +02:00
John Livingston
a9ae96622a
Translated using Weblate (German)
Currently translated at 98.9% (281 of 284 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-08-06 17:35:19 +02:00
Victor Hampel
4afc0b6ab8
Translated using Weblate (German)
Currently translated at 100.0% (284 of 284 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-08-06 17:35:18 +02:00
John Livingston
ad5397b3c7
Adding actions on the occupant list. 2024-08-06 17:32:37 +02:00
John Livingston
49e11d2b6b
Include a Converse fix. 2024-08-06 15:50:40 +02:00
John Livingston
df7981f896
Including a Converse Fix. 2024-08-06 12:25:26 +02:00
John Livingston
003cb24dd8
Updating Converse upstream (with bootstrap5):
* bootstrap 5 compatibility
* other Converse updates integration
* hack to get the sidebar work as with Converse v10.
* modal onHide was renamed close.
* fix slow mode infobox margin.
* fix margin
* shorter action label, for better dropdown UX.
2024-08-06 12:04:28 +02:00
John Livingston
33da4314af
Merge pull request #490 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-08-06 11:46:13 +02:00
Victor Hampel
c2c1211b9a
Translated using Weblate (German)
Currently translated at 100.0% (857 of 857 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-08-06 11:45:27 +02:00
Victor Hampel
5d843ebf92
Translated using Weblate (German)
Currently translated at 100.0% (851 of 851 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-08-06 11:45:27 +02:00
John Livingston
18fa3aec10
Merge pull request #489 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-08-06 11:45:20 +02:00
John Livingston
631d8c7a6b
Translated using Weblate (French)
Currently translated at 100.0% (284 of 284 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/fr/
2024-08-05 19:39:57 +02:00
John Livingston
9746f3d86e
Translated using Weblate (English)
Currently translated at 100.0% (284 of 284 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/en/
2024-08-05 19:39:57 +02:00
Victor Hampel
d412f86577
Translated using Weblate (German)
Currently translated at 100.0% (282 of 282 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-08-05 12:10:36 +02:00
John Livingston
e665823f5b
Message search documentation (#145) 2024-08-05 12:08:45 +02:00
John Livingston
966669ebbc
Search user messages WIP (#145) 2024-08-05 11:44:11 +02:00
John Livingston
4181661faf
Search user messages WIP (#145) 2024-08-01 18:58:25 +02:00
John Livingston
dd03075831
Fix naming. 2024-08-01 17:14:07 +02:00
John Livingston
a4497739fa
Search user messages WIP (#145) 2024-08-01 16:54:31 +02:00
John Livingston
cdbe97137e
Moderation notes technical documentation. 2024-07-31 23:28:10 +02:00
John Livingston
7892fd6c03
Fix doc link + add missing chapter. 2024-07-31 23:19:48 +02:00
John Livingston
0543a720f2
Fix REUSE. 2024-07-31 23:00:29 +02:00
John Livingston
af2941f4e0
npm run doc:translate 2024-07-31 22:50:38 +02:00
John Livingston
3004105b5e
Moderator notes WIP (#144):
* user documentation
2024-07-31 22:48:16 +02:00
John Livingston
bb2aca71c1
Moderator notes WIP (#144):
* Fix notes occupants when unknown
* notes.getAboutOccupant result cache
2024-07-31 22:48:16 +02:00
John Livingston
48763e6173
Moderator notes WIP (#144):
Displaying the nickname at time of note creation if it changed.
2024-07-31 22:48:16 +02:00
John Livingston
70f702f78e
Fix: clicking on the current user nickname in message history was failing to open the profile modal. 2024-07-31 22:48:16 +02:00
John Livingston
a46425d51f
Moderator notes WIP (#144) 2024-07-31 22:48:15 +02:00
John Livingston
e81a7c90b8
Adding the commit id for jcbrand/bootstrap5 (commented) 2024-07-31 22:48:15 +02:00
John Livingston
9c2b84027a
Moderator notes WIP (#144) 2024-07-31 22:48:15 +02:00
John Livingston
704e660f37
Moderator notes WIP (#144) 2024-07-31 22:48:15 +02:00
John Livingston
31c4e5a646
Updating Converse upstream. 2024-07-31 22:48:15 +02:00
John Livingston
1c749f68bc
Fix new task order + fix notes order. 2024-07-31 22:48:15 +02:00
John Livingston
fbc9a39485
Refactoring: moving the draggable code in a common class. 2024-07-31 22:48:15 +02:00
John Livingston
eb76e7ebb9
Moderator notes WIP (#144) 2024-07-31 22:48:14 +02:00
John Livingston
20cb668e09
Muc-app: some refactoring. 2024-07-31 22:48:14 +02:00
John Livingston
86cac34ef3
Muc-app: cleaning code. 2024-07-31 22:48:14 +02:00
John Livingston
074e688ed8
New moderator app WIP:
* #144: moderator notes WIP,
* plugin size: adding an API,
* refactoring the code from the task app, to create a new MUC App
  system.
2024-07-31 22:48:14 +02:00
John Livingston
34da786b65
Merge pull request #485 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-07-31 22:47:49 +02:00
Victor Hampel
a700263eda
Translated using Weblate (German)
Currently translated at 100.0% (272 of 272 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-07-31 22:47:37 +02:00
John Livingston
1b92e7287d
Merge pull request #484 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-07-31 22:47:31 +02:00
Victor Hampel
bf8f3a08ec
Translated using Weblate (German)
Currently translated at 100.0% (812 of 812 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-07-30 08:49:55 +02:00
John Livingston
faef584f8b
npm run doc:translate 2024-07-29 15:00:02 +02:00
John Livingston
ce5114afc9
mod_muc_anonymize_moderation_actions: fix XEP-0425 v0.2.1 compliance. 2024-07-29 15:00:02 +02:00
John Livingston
c5bcb9fc14
Fix mod_muc_moderation + anonymize moderation events (#137) 2024-07-29 15:00:02 +02:00
John Livingston
6e92882176
Fix mod_muc_moderation regression:
See https://issues.prosody.im/1862
2024-07-29 15:00:02 +02:00
John Livingston
ebc8fc8797
Option to hide moderator name who made actions WIP (#137). 2024-07-29 15:00:02 +02:00
John Livingston
38f2b2af57
prosody-modules: preparing some modules for publication on
prosody-modules repo
2024-07-29 15:00:02 +02:00
John Livingston
b7c595214b
Updated mod_muc_moderation to upstream. 2024-07-29 15:00:01 +02:00
John Livingston
58676a5508
Merge pull request #482 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-07-29 14:59:01 +02:00
John Livingston
12f11e4468
Translated using Weblate (French)
Currently translated at 100.0% (270 of 270 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/fr/
2024-07-26 13:07:22 +02:00
Victor Hampel
e1252709b3
Translated using Weblate (German)
Currently translated at 100.0% (270 of 270 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/de/
2024-07-26 10:58:36 +02:00
John Livingston
80d458c445
Changelog. 2024-07-26 10:58:26 +02:00
John Livingston
f88520d925
Implements #146: copy message button for moderators
We overload the copy message method that comes with Converse 11, to add
the message metadata (nick and full date).
2024-07-26 10:51:55 +02:00
John Livingston
dd4bca8c06
Fix Converse v11 regression - occupant comparator function:
The way we overloaded the MUCOccupants method was no more working.
2024-07-25 18:32:38 +02:00
John Livingston
81632fa467
livechat-specific Converse plugin: refactoring 2024-07-25 18:03:46 +02:00
John Livingston
c6c365abf0
Avatar set for anonymous users: new 'none' choice (that will fallback to Converse new colorized avatars). 2024-07-25 15:34:27 +02:00
John Livingston
099ff28c76
Merge branch 'release/10.3.x' 2024-07-25 12:17:20 +02:00
John Livingston
b40b3a2716
Changelog. 2024-07-25 11:12:49 +02:00
John Livingston
731be16e53
Merge pull request #479 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-07-25 11:10:31 +02:00
Victor Hampel
a7bd0c1c3e
Translated using Weblate (German)
Currently translated at 100.0% (809 of 809 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-07-25 11:10:11 +02:00
Milo Ivir
e938f79182
Translated using Weblate (Croatian)
Currently translated at 13.1% (106 of 809 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/hr/
2024-07-25 11:10:11 +02:00
John Livingston
36323569c0
Translated using Weblate (French)
Currently translated at 100.0% (809 of 809 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/fr/
2024-07-25 11:10:11 +02:00
John Livingston
e7b1376a43
Merge pull request #480 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-07-25 11:10:04 +02:00
T.S
1b4ccf6693
Translated using Weblate (Japanese)
Currently translated at 63.5% (171 of 269 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/ja/
2024-07-20 15:42:47 +02:00
Milo Ivir
d4ecafb6de
Translated using Weblate (Croatian)
Currently translated at 90.3% (243 of 269 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/hr/
2024-07-19 21:15:39 +02:00
John Livingston
20550bc3d7
Merge pull request #478 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-07-18 16:45:40 +02:00
Besnik Bleta
548b79a3a6
Translated using Weblate (Albanian)
Currently translated at 71.7% (193 of 269 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/sq/
2024-07-18 16:43:55 +02:00
John Livingston
ed0b2eb913
ConverseJS merge. 2024-07-18 10:39:40 +02:00
John Livingston
bdc11cb92e
Merge branch 'release/10.3.x' 2024-07-18 10:29:12 +02:00
John Livingston
8b0d72bf13
Fix Converse bug. 2024-07-17 11:48:43 +02:00
John Livingston
64f03e5454
Documentation. 2024-07-17 10:53:53 +02:00
John Livingston
522265db5c
Changelog. 2024-07-17 10:48:14 +02:00
John Livingston
22daa45b92
Converse update. 2024-07-17 10:47:30 +02:00
John Livingston
76cd519c00
Merge pull request #475 from framabot/weblate-peertube-livechat-peertube-plugin-livechat
Translations update from Framasoft Weblate
2024-07-17 10:46:26 +02:00
Besnik Bleta
123f9a5a8a
Translated using Weblate (Albanian)
Currently translated at 62.4% (168 of 269 strings)

Translation: PeerTube LiveChat/Peertube Plugin LiveChat
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat/sq/
2024-07-16 14:36:05 +02:00
John Livingston
717a5c75de
Merge pull request #472 from framabot/weblate-peertube-livechat-peertube-plugin-livechat-documentation
Translations update from Framasoft Weblate
2024-07-16 12:16:00 +02:00
Victor Hampel
12a1300df6
Translated using Weblate (German)
Currently translated at 100.0% (808 of 808 strings)

Translation: PeerTube LiveChat/Peertube Plugin Livechat Documentation
Translate-URL: https://weblate.framasoft.org/projects/peertube-livechat/peertube-plugin-livechat-documentation/de/
2024-07-16 12:15:04 +02:00
John Livingston
5fb50b9221
Merge branch 'release/10.3.x' 2024-07-16 12:14:51 +02:00
John Livingston
00960652fe
Fix «create new poll» icon. 2024-07-16 12:01:51 +02:00
John Livingston
712d2bcdcb
Converse Fix. 2024-07-16 11:35:26 +02:00
John Livingston
897f111e9d
Merge pull request #474 from JohnXLivingston/converse_v11_upstream
Switching to Converse v11 upstream
2024-07-15 18:19:14 +02:00
John Livingston
2f69e45b26
Changelog. 2024-07-15 18:15:45 +02:00
John Livingston
b9473cada9
Converse upstream WIP:
* moving exports to _converse.exports.
2024-07-15 17:53:57 +02:00
John Livingston
cbcf51d1eb
Converse upstream WIP:
* Fix anonymous mode
2024-07-15 17:42:32 +02:00
John Livingston
1226162b50
Converse upstream WIP. 2024-07-15 17:35:26 +02:00
John Livingston
f1ac80d468
Converse v11, reporting customization in the livechat repo:
Destroy room: remove the challenge, and the new JID.
2024-07-15 16:50:26 +02:00
John Livingston
e8f287b8a9
Converse v11, reporting customization in the livechat repo:
Reporting the toggle occupants customization.
2024-07-15 16:27:01 +02:00
John Livingston
e97cd1d78e
Converse v11, reporting customization in the livechat repo:
Reporting the vcard plugin customizations in the livechat repo, to avoid
maintaining a Converse fork.
2024-07-15 15:57:28 +02:00
John Livingston
6218d65b72
Converse upstream WIP. 2024-07-15 14:20:44 +02:00
John Livingston
d0ab3d94ae
Converse upstream updates. 2024-07-15 12:09:25 +02:00
John Livingston
a0d5c4a368
Switch from Converse v10.1.6 to upstream (unreleased v11):
* fix poll form
2024-07-11 18:22:59 +02:00
John Livingston
51b603c894
Switch from Converse v10.1.6 to upstream (unreleased v11):
* various WIP to change the Converse version
2024-07-11 17:53:50 +02:00
John Livingston
9679aec739
Moderation delay: fix accessibility on the timer shown to moderators. 2024-07-11 15:42:12 +02:00
251 changed files with 30235 additions and 6952 deletions

View File

@ -33,7 +33,7 @@ jobs:
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.80.0'
hugo-version: '0.132.2'
extended: true
- name: Generate documentation translations

View File

@ -22,7 +22,7 @@ pages:
image: registry.gitlab.com/pages/hugo/hugo_extended:latest
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-learn
GIT_SUBMODULE_PATHS: support/documentation/themes/hugo-theme-relearn
script:
# gitlab need the generated documentation to be in the /public dir.
- hugo -s support/documentation/ --minify -d ../../public/ --baseURL='https://livingston.frama.io/peertube-plugin-livechat/'

6
.gitmodules vendored
View File

@ -2,6 +2,6 @@
#
# SPDX-License-Identifier: AGPL-3.0-only
[submodule "documentation/themes/hugo-theme-learn"]
path = support/documentation/themes/hugo-theme-learn
url = https://github.com/matcornic/hugo-theme-learn.git
[submodule "support/documentation/themes/hugo-theme-relearn"]
path = support/documentation/themes/hugo-theme-relearn
url = https://github.com/McShelby/hugo-theme-relearn.git

View File

@ -32,3 +32,7 @@ License: AGPL-3.0-only
Files: .github/PULL_REQUEST_TEMPLATE.md
Copyright: 2024 John Livingston <https://www.john-livingston.fr/>
License: AGPL-3.0-only
Files: prosody-modules/mod_firewall/*
Copyright: Prosody Community Modules <https://modules.prosody.im/mod_firewall>
License: MIT

View File

@ -1,5 +1,42 @@
# Changelog
## 11.0.1
### Minor changes and fixes
* Fix "send message" button that was sending the message twice.
## 11.0.0
### Importante Notes
With the new [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) feature, Peertube admins can write firewall rules for the Prosody server. These rules could be used to run arbitrary code on the server. If you are a hosting provider, and you don't want to allow Peertube admins to write such rules, you can disable the online editing by creating a `disable_mod_firewall_editing` file in the plugin directory. Check the documentation for more information. This is opt-out, as Peertube admins can already run arbitrary code just by installing any plugin.
The concord theme was removed from ConverseJS. If you had it set in the plugin settings, it will fallback to the Peertube theme.
### New features
* Updating ConverseJS, to use upstream (v11 WIP). This comes with many improvements and new features.
* #146: copy message button for moderators.
* #137: option to hide moderator name who made actions (kick, ban, message moderation, ...).
* #144: [moderator notes](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation_notes/).
* #145: action for moderators to find all messages from a given participant.
* #97: option to use and configure [mod_firewall](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/mod_firewall/) at the server level.
### Minor changes and fixes
* #118: improved accessibility.
* Avatar set for anonymous users: new 'none' choice (that will fallback to Converse new colorized avatars).
* New translation: Albanian.
* Translation updates: Crotian, Japanese, traditional Chinese, Arabic, Galician.
* Updated mod_muc_moderation to upstream.
* Fix new task ordering.
* Fix: clicking on the current user nickname in message history was failing to open the profile modal.
* Fix: increase chat height on small screens, try to better detect the device viewport size and orientation.
* Converse theme: removed concord, added cyberpunk.
* Fixed Converse theme settings localization.
* Fix: improved minimum chat width.
## 10.3.3
### Minor changes and fixes

View File

@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* stylelint-disable custom-property-pattern */
@use "sass:color";
@use "../../variables";
.peertube-plugin-livechat-admin-firewall {
h1 {
padding-top: 40px;
/* See Peertube sub-menu-h1 mixin */
font-size: 1.3rem;
border-bottom: 2px solid var(--greyBackgroundColor);
padding-bottom: 15px;
}
textarea[name^="_content_"] {
min-height: 10rem;
}
input[type="submit"],
input[type="reset"],
button[type="submit"],
button[type="reset"] {
// Peertube rounded-line-height-1-5 mixins
line-height: variables.$button-calc-line-height;
// Peertube peertube-button mixin
padding: 4px 13px;
border: 0;
font-weight: variables.$font-semibold;
border-radius: 3px !important;
text-align: center;
cursor: pointer;
font-size: variables.$button-font-size;
}
input[type="submit"],
button[type="submit"] {
// Peertube orange-button mixin
&,
&:active,
&:focus {
color: #fff;
background-color: var(--mainColor);
}
&:hover {
color: #fff;
background-color: var(--mainHoverColor);
}
&[disabled],
&.disabled {
cursor: default;
color: #fff;
background-color: var(--inputBorderColor);
}
}
input[type="reset"],
button[type="reset"] {
// Peertube grey-button mixin
background-color: var(--greyBackgroundColor);
color: var(--greyForegroundColor);
&:hover,
&:active,
&:focus,
&[disabled],
&.disabled {
color: var(--greyForegroundColor);
background-color: var(--greySecondaryBackgroundColor);
}
&[disabled],
&.disabled {
cursor: default;
}
}
.peertube-livechat-admin-firewall-col-name {
width: 25%;
}
.peertube-livechat-admin-firewall-col-content {
width: 65%;
}
}

View File

@ -15,9 +15,9 @@ livechat-spinner,
height: 48px;
margin: 20px;
/* stylelint-disable-next-line custom-property-pattern */
border: 5px solid var(--greyBackgroundColor);
border: 5px solid var(--greyBackgroundColor) !important; // !important is required for it to work in ConverseJS
/* stylelint-disable-next-line custom-property-pattern */
border-bottom-color: var(--mainColor);
border-bottom-color: var(--mainColor) !important; // !important is required for it to work in ConverseJS
border-radius: 50%;
display: inline-block;
box-sizing: border-box;

View File

@ -9,4 +9,5 @@
@use "elements/index";
@use "video";
@use "configuration/configuration";
@use "admin/firewall/firewall";
@use "list-rooms/list-rooms.scss";

View File

@ -18,17 +18,31 @@
/* Note: livechat-viewer-mode-content (the form where anonymous users can
choose nickname or log in with external account), can be something like
~180px height (at time of writing).
We must ensure that the 200px limit for converse-muc and converse-root is
We must ensure that the px height limit for converse-muc and converse-root is
always higher than livechat-viewer-mode-content max size.
Note: We also must ensure that when the user has choosen its nickname, and there is an
ongoing poll, the user can see the chat when the poll is folded.
*/
#peertube-plugin-livechat-container converse-root {
display: block;
border: 1px solid black;
min-height: max(30vh, 200px); // Always at least 200px, and ideally at least 30% of viewport.
min-height: max(30vh, 300px); // Always at least 200px, and ideally at least 30% of viewport.
height: 100%;
min-width: min(400px, 25vw);
converse-muc {
min-height: max(59vh, 400px);
min-height: max(30vh, 300px);
}
@media screen and (orientation: portrait) and (max-width: 767px) {
/* On small screen, and when portrait mode, we are giving the chat more vertical space.
It should go under the video.
*/
min-height: max(58vh, 300px);
converse-muc {
min-height: max(58vh, 300px);
}
}
}

View File

@ -12,6 +12,7 @@ declare const MUSTACHE_CONFIGURATION_CHANNEL: string
// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file.
// See the online documentation: https://livingston.frama.io/peertube-plugin-livechat/contributing/translate/
declare const LOC_ONLINE_HELP: string
declare const LOC_CHAT: string
declare const LOC_OPEN_CHAT: string
declare const LOC_OPEN_CHAT_NEW_WINDOW: string
declare const LOC_CLOSE_CHAT: string
@ -133,3 +134,13 @@ declare const LOC_POLL_VOTE_OK: string
declare const LOC_MODERATION_DELAY: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC: string
declare const LOC_PROSODY_FIREWALL_CONFIGURATION: string
declare const LOC_PROSODY_FIREWALL_CONFIGURATION_HELP: string
declare const LOC_PROSODY_FIREWALL_DISABLED_WARNING: string
declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
declare const LOC_PROSODY_FIREWALL_NAME: string
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
declare const LOC_PROSODY_FIREWALL_CONTENT: string

View File

@ -270,6 +270,8 @@ function register (clientOptions: RegisterClientOptions): void {
return !(options.formValues['chat-all-lives'] === true && options.formValues['chat-per-live-video'] === true)
case 'auto-ban-anonymous-ip':
return options.formValues['chat-no-anonymous'] !== false
case 'prosody-firewall-configure-button':
return options.formValues['prosody-firewall-enabled'] !== true
}
if (name?.startsWith('external-auth-')) {

View File

@ -8,6 +8,7 @@ import { registerConfiguration } from './common/configuration/register'
import { registerVideoWatch } from './common/videowatch/register'
import { registerRoom } from './common/room/register'
import { initPtContext } from './common/lib/contexts/peertube'
import { registerAdminFirewall } from './common/admin/firewall/register'
import './common/lib/elements' // Import shared elements.
async function register (clientOptions: RegisterClientOptions): Promise<void> {
@ -69,7 +70,8 @@ async function register (clientOptions: RegisterClientOptions): Promise<void> {
await Promise.all([
registerVideoWatch(),
registerRoom(clientOptions),
registerConfiguration(clientOptions)
registerConfiguration(clientOptions),
registerAdminFirewall(clientOptions)
])
}

View File

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { AdminFirewallConfiguration } from 'shared/lib/types'
import { AdminFirewallService } from '../services/admin-firewall'
import { LivechatElement } from '../../../lib/elements/livechat'
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
import { tplAdminFirewall } from '../templates/admin-firewall'
import { TemplateResult, html, nothing } from 'lit'
import { customElement, state } from 'lit/decorators.js'
import { Task } from '@lit/task'
@customElement('livechat-admin-firewall')
export class AdminFirewallElement extends LivechatElement {
private _adminFirewallService?: AdminFirewallService
@state()
public firewallConfiguration?: AdminFirewallConfiguration
@state()
public validationError?: ValidationError
@state()
public actionDisabled: boolean = false
private _asyncTaskRender: Task
constructor () {
super()
this._asyncTaskRender = this._initTask()
}
protected _initTask (): Task {
return new Task(this, {
task: async () => {
this._adminFirewallService = new AdminFirewallService(this.ptOptions)
this.firewallConfiguration = await this._adminFirewallService.fetchConfiguration()
this.actionDisabled = false // in case of reset
},
args: () => []
})
}
/**
* Resets the form by reloading data from backend.
*/
public async reset (event?: Event): Promise<void> {
event?.preventDefault()
this.actionDisabled = true
this._asyncTaskRender = this._initTask()
this.requestUpdate()
}
/**
* Resets the validation errors.
* @param ev the vent
*/
public resetValidation (_ev?: Event): void {
if (this.validationError) {
this.validationError = undefined
this.requestUpdate('_validationError')
}
}
/**
* Saves the configuration.
* @param event event
*/
public readonly saveConfig = async (event?: Event): Promise<void> => {
event?.preventDefault()
if (!this.firewallConfiguration || !this._adminFirewallService) {
return
}
this.actionDisabled = true
this._adminFirewallService.saveConfiguration(this.firewallConfiguration)
.then((result: AdminFirewallConfiguration) => {
this.validationError = undefined
this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => {
this.ptNotifier.info(msg)
}, () => {})
this.firewallConfiguration = result
this.requestUpdate('firewallConfiguration')
this.requestUpdate('_validationError')
})
.catch(async (error: Error) => {
this.validationError = undefined
if (error instanceof ValidationError) {
this.validationError = error
}
this.logger.warn(`A validation error occurred in saving configuration. ${error.name}: ${error.message}`)
this.ptNotifier.error(
error.message
? error.message
: await this.ptTranslate(LOC_ERROR)
)
this.requestUpdate('_validationError')
})
.finally(() => {
this.actionDisabled = false
})
}
public readonly getInputValidationClass = (propertyName: string): { [key: string]: boolean } => {
const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[`${propertyName}`]
return validationErrorTypes ? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true }) : {}
}
public readonly renderFeedback = (feedbackId: string,
propertyName: string): TemplateResult | typeof nothing => {
const errorMessages: TemplateResult[] = []
const validationErrorTypes: ValidationErrorType[] | undefined =
this.validationError?.properties[`${propertyName}`] ?? undefined
// FIXME: this code is duplicated in dymamic table form
if (validationErrorTypes && validationErrorTypes.length !== 0) {
return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>`
} else {
return nothing
}
}
protected override render = (): unknown => {
return this._asyncTaskRender.render({
pending: () => html`<livechat-spinner></livechat-spinner>`,
error: () => html`<livechat-error></livechat-error>`,
complete: () => tplAdminFirewall(this)
})
}
}

View File

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import './admin-firewall'

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import { html, render } from 'lit'
import './elements' // Import all needed elements.
/**
* Registers stuff related to mod_firewall configuration.
* @param clientOptions Peertube client options
*/
async function registerAdminFirewall (clientOptions: RegisterClientOptions): Promise<void> {
const { registerClientRoute } = clientOptions
registerClientRoute({
route: 'livechat/admin/firewall',
onMount: async ({ rootEl }) => {
render(html`<livechat-admin-firewall .registerClientOptions=${clientOptions}></livechat-admin-firewall>`, rootEl)
}
})
}
export {
registerAdminFirewall
}

View File

@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
import type { AdminFirewallConfiguration } from 'shared/lib/types'
import {
maxFirewallFileSize, maxFirewallNameLength, maxFirewallFiles, firewallNameRegexp
} from 'shared/lib/admin-firewall'
import { ValidationError, ValidationErrorType } from '../../../lib/models/validation'
import { getBaseRoute } from '../../../../utils/uri'
export class AdminFirewallService {
public _registerClientOptions: RegisterClientOptions
private readonly _headers: any = {}
constructor (registerClientOptions: RegisterClientOptions) {
this._registerClientOptions = registerClientOptions
this._headers = this._registerClientOptions.peertubeHelpers.getAuthHeader() ?? {}
this._headers['content-type'] = 'application/json;charset=UTF-8'
}
async validateConfiguration (adminFirewallConfiguration: AdminFirewallConfiguration): Promise<boolean> {
const propertiesError: ValidationError['properties'] = {}
if (adminFirewallConfiguration.files.length > maxFirewallFiles) {
const validationError = new ValidationError(
'AdminFirewallConfigurationValidationError',
await this._registerClientOptions.peertubeHelpers.translate(LOC_TOO_MANY_ENTRIES),
propertiesError
)
throw validationError
}
const seen = new Map<string, true>()
for (const [i, e] of adminFirewallConfiguration.files.entries()) {
propertiesError[`files.${i}.name`] = []
if (e.name === '') {
propertiesError[`files.${i}.name`].push(ValidationErrorType.Missing)
} else if (e.name.length > maxFirewallNameLength) {
propertiesError[`files.${i}.name`].push(ValidationErrorType.TooLong)
} else if (!firewallNameRegexp.test(e.name)) {
propertiesError[`files.${i}.name`].push(ValidationErrorType.WrongFormat)
} else if (seen.has(e.name)) {
propertiesError[`files.${i}.name`].push(ValidationErrorType.Duplicate)
} else {
seen.set(e.name, true)
}
propertiesError[`files.${i}.content`] = []
if (e.content.length > maxFirewallFileSize) {
propertiesError[`files.${i}.content`].push(ValidationErrorType.TooLong)
}
}
if (Object.values(propertiesError).find(e => e.length > 0)) {
const validationError = new ValidationError(
'AdminFirewallConfigurationValidationError',
await this._registerClientOptions.peertubeHelpers.translate(LOC_VALIDATION_ERROR),
propertiesError
)
throw validationError
}
return true
}
async saveConfiguration (
adminFirewallConfiguration: AdminFirewallConfiguration
): Promise<AdminFirewallConfiguration> {
if (!await this.validateConfiguration(adminFirewallConfiguration)) {
throw new Error('Invalid form data')
}
const response = await fetch(
getBaseRoute(this._registerClientOptions) + '/api/admin/firewall/',
{
method: 'POST',
headers: this._headers,
body: JSON.stringify(adminFirewallConfiguration)
}
)
if (!response.ok) {
throw new Error('Failed to save configuration.')
}
return response.json()
}
async fetchConfiguration (): Promise<AdminFirewallConfiguration> {
const response = await fetch(
getBaseRoute(this._registerClientOptions) + '/api/admin/firewall/',
{
method: 'GET',
headers: this._headers
}
)
if (!response.ok) {
throw new Error('Can\'t get firewall configuration.')
}
return response.json()
}
}

View File

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { AdminFirewallElement } from '../elements/admin-firewall'
import type { TemplateResult } from 'lit'
import type { DynamicFormHeader, DynamicFormSchema } from '../../../lib/elements/dynamic-table-form'
import { maxFirewallFiles, maxFirewallNameLength, maxFirewallFileSize } from 'shared/lib/admin-firewall'
import { ptTr } from '../../../lib/directives/translation'
import { html } from 'lit'
export function tplAdminFirewall (el: AdminFirewallElement): TemplateResult {
const tableHeaderList: DynamicFormHeader = {
enabled: {
colName: ptTr(LOC_PROSODY_FIREWALL_FILE_ENABLED)
},
name: {
colName: ptTr(LOC_PROSODY_FIREWALL_NAME),
description: ptTr(LOC_PROSODY_FIREWALL_NAME_DESC),
headerClassList: ['peertube-livechat-admin-firewall-col-name']
},
content: {
colName: ptTr(LOC_PROSODY_FIREWALL_CONTENT),
headerClassList: ['peertube-livechat-admin-firewall-col-content']
}
}
const tableSchema: DynamicFormSchema = {
enabled: {
inputType: 'checkbox',
default: true
},
name: {
inputType: 'text',
default: '',
maxlength: maxFirewallNameLength
},
content: {
inputType: 'textarea',
default: '',
maxlength: maxFirewallFileSize
}
}
return html`
<div class="margin-content peertube-plugin-livechat-admin-firewall">
<h1>
${ptTr(LOC_PROSODY_FIREWALL_CONFIGURATION)}
</h1>
<p>
${ptTr(LOC_PROSODY_FIREWALL_CONFIGURATION_HELP, true)}
<livechat-help-button .page=${'documentation/admin/mod_firewall'}>
</livechat-help-button>
</p>
${
el.firewallConfiguration?.enabled
? ''
: html`<p class="peertube-plugin-livechat-warning">${ptTr(LOC_PROSODY_FIREWALL_DISABLED_WARNING, true)}</p>`
}
<form role="form" @submit=${el.saveConfig} @change=${el.resetValidation}>
<livechat-dynamic-table-form
.header=${tableHeaderList}
.schema=${tableSchema}
.maxLines=${maxFirewallFiles}
.validation=${el.validationError?.properties}
.validationPrefix=${'files'}
.rows=${el.firewallConfiguration?.files}
@update=${(e: CustomEvent) => {
el.resetValidation(e)
if (el.firewallConfiguration) {
el.firewallConfiguration.files = e.detail
el.requestUpdate('firewallConfiguration')
}
}
}
></livechat-dynamic-table-form>
<div class="form-group mt-5">
<button type="reset" @click=${el.reset} ?disabled=${el.actionDisabled}>
${ptTr(LOC_CANCEL)}
</button>
<button type="submit" ?disabled=${el.actionDisabled}>
${ptTr(LOC_SAVE)}
</button>
</div>
</form>
</div>`
}

View File

@ -50,7 +50,7 @@ export class ChannelHomeElement extends LivechatElement {
<ul class="peertube-plugin-livechat-configuration-home-channels">
${this._channels?.map((channel) => html`
<li>
<a href="${channel.livechatConfigurationUri}">
<a href="${channel.livechatConfigurationUri}" aria-hidden="true">
${channel.avatar
? html`<img class="avatar channel" src="${channel.avatar.path}">`
: html`<div class="avatar channel initial gray"></div>`

View File

@ -135,6 +135,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
</livechat-configuration-section-header>
<div class="form-group">
<textarea
.title=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL) as any}
name="terms"
id="peertube-livechat-terms"
.value=${el.channelConfiguration?.configuration.terms ?? ''}
@ -167,7 +168,7 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
<label>
<input
type="checkbox"
name="bot"
name="mute_anonymous"
id="peertube-livechat-mute-anonymous"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
@ -254,6 +255,32 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
</div>
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC, true)}
.helpPage=${'documentation/user/streamers/moderation'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="anonymize-moderation"
id="peertube-livechat-anonymize-moderation"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.moderation.anonymize =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.moderation.anonymize}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_LABEL)}
</label>
</div>
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
.description=${''}

View File

@ -6,6 +6,7 @@
// This content comes from the file assets/images/plus-square.svg, from the Feather icons set https://feathericons.com/
export const AddSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-plus-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
@ -15,6 +16,7 @@ export const AddSVG: string =
// This content comes from the file assets/images/x-square.svg, from the Feather icons set https://feathericons.com/
export const RemoveSVG: string =
`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
aria-hidden="true"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-x-square">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>

View File

@ -47,11 +47,11 @@ interface CellDataSchema {
minlength?: number
maxlength?: number
size?: number
label?: TemplateResult | string
options?: { [key: string]: string }
datalist?: DynamicTableAcceptedTypes[]
separator?: string
inputType?: DynamicTableAcceptedInputTypes
inputTitle?: string
default?: DynamicTableAcceptedTypes
colClassList?: string[] // CSS classes to add to the <td> element.
}
@ -64,7 +64,7 @@ interface DynamicTableRowData {
interface DynamicFormHeaderCellData {
colName: TemplateResult | DirectiveResult
description: TemplateResult | DirectiveResult
description?: TemplateResult | DirectiveResult
headerClassList?: string[]
}
@ -236,7 +236,7 @@ export class DynamicTableFormElement extends LivechatElement {
classList.push(...headerCellData.headerClassList)
}
return html`<th scope="col" class=${classList.join(' ')}>
${headerCellData.description}
${headerCellData.description ?? ''}
</th>`
}
@ -295,6 +295,7 @@ export class DynamicTableFormElement extends LivechatElement {
const inputId =
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
const inputTitle: DirectiveResult | undefined = propertySchema.inputTitle ?? this.header[propertyName]?.colName
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
switch (propertySchema.default?.constructor) {
@ -320,6 +321,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -332,6 +334,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderTextarea(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -344,6 +347,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderSelect(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -356,6 +360,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderImageFileInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue?.toString(),
@ -376,6 +381,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
(propertyValue as Date).toISOString(),
@ -394,6 +400,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as string,
@ -411,6 +418,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderCheckbox(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue as boolean,
@ -446,6 +454,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ??
@ -461,6 +470,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderTextarea(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
(propertyValue)?.join(propertySchema.separator ?? ',') ??
@ -476,6 +486,7 @@ export class DynamicTableFormElement extends LivechatElement {
formElement = html`${this._renderTagsInput(rowId,
inputId,
inputName,
inputTitle,
propertyName,
propertySchema,
propertyValue,
@ -501,6 +512,7 @@ export class DynamicTableFormElement extends LivechatElement {
_renderInput = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -515,6 +527,7 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
min=${ifDefined(propertySchema.min)}
@ -534,6 +547,7 @@ export class DynamicTableFormElement extends LivechatElement {
_renderTagsInput = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: Array<string | number>,
@ -547,7 +561,7 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
.inputPlaceholder=${propertySchema.label as any}
.inputTitle=${inputTitle as any}
aria-describedby="${inputId}-feedback"
.min=${propertySchema.min}
.max=${propertySchema.max}
@ -563,6 +577,7 @@ export class DynamicTableFormElement extends LivechatElement {
_renderTextarea = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -576,6 +591,7 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
min=${ifDefined(propertySchema.min)}
max=${ifDefined(propertySchema.max)}
@ -588,6 +604,7 @@ export class DynamicTableFormElement extends LivechatElement {
_renderCheckbox = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: boolean,
@ -602,6 +619,7 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
value="1"
@ -611,6 +629,7 @@ export class DynamicTableFormElement extends LivechatElement {
_renderSelect = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -623,11 +642,12 @@ export class DynamicTableFormElement extends LivechatElement {
)
)}
id=${inputId}
title=${ifDefined(inputTitle)}
aria-describedby="${inputId}-feedback"
aria-label=${inputName}
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
>
<option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</option>
<option ?selected=${!propertyValue}>${inputTitle ?? ''}</option>
${Object.entries(propertySchema.options ?? {})
?.map(([value, name]) =>
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
@ -638,6 +658,7 @@ export class DynamicTableFormElement extends LivechatElement {
_renderImageFileInput = (rowId: number,
inputId: string,
inputName: string,
inputTitle: string | DirectiveResult | undefined,
propertyName: string,
propertySchema: CellDataSchema,
propertyValue: string,
@ -647,6 +668,7 @@ export class DynamicTableFormElement extends LivechatElement {
.name=${inputName}
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
id=${inputId}
.inputTitle=${inputTitle as any}
aria-describedby="${inputId}-feedback"
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
.value=${propertyValue}

View File

@ -1,11 +1,11 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { LivechatElement } from './livechat'
import { html } from 'lit'
import type { DirectiveResult } from 'lit/directive'
import { customElement, property } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
/**
* Special element to upload image files.
* If no current value, displays an input type="file" field.
@ -29,13 +29,16 @@ export class ImageFileInputElement extends LivechatElement {
@property({ attribute: false })
public maxSize?: number
@property({ attribute: false })
public inputTitle?: string | DirectiveResult
@property({ attribute: false })
public accept: string[] = ['image/jpg', 'image/png', 'image/gif']
protected override render = (): unknown => {
return html`
${this.value
? html`<img src=${this.value} @click=${(ev: Event) => {
? html`<img src=${this.value} alt=${ifDefined(this.inputTitle)} @click=${(ev: Event) => {
ev.preventDefault()
const upload: HTMLInputElement | null | undefined = this.parentElement?.querySelector('input[type="file"]')
upload?.click()
@ -44,6 +47,7 @@ export class ImageFileInputElement extends LivechatElement {
}
<input
type="file"
title=${ifDefined(this.inputTitle)}
accept="${this.accept.join(',')}"
class="form-control"
style=${this.value ? 'display: none;' : ''}

View File

@ -12,6 +12,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'
import { classMap } from 'lit/directives/class-map.js'
import { animate, fadeOut, fadeIn } from '@lit-labs/motion'
import { repeat } from 'lit/directives/repeat.js'
import type { DirectiveResult } from 'lit/directive'
// FIXME: find a better way to store this image.
// This content comes from the file assets/images/copy.svg, after svgo cleaning.
@ -48,7 +49,7 @@ export class TagsInputElement extends LivechatElement {
private _inputValue?: string = ''
@property({ attribute: false })
public inputPlaceholder?: string = ''
public inputTitle?: string | DirectiveResult = ''
@property({ attribute: false })
public datalist?: string[]
@ -166,7 +167,7 @@ export class TagsInputElement extends LivechatElement {
@input=${(e: InputEvent) => this._handleInputEvent(e)}
@change=${(e: Event) => e.stopPropagation()}
.value=${this._inputValue ?? ''}
placeholder=${ifDefined(this.inputPlaceholder)} />
title=${ifDefined(this.inputTitle)} />
${(this.datalist)
? html`<datalist id="${this.id ?? 'tags-input'}-datalist">
${(this.datalist ?? []).map((value) => html`<option value=${value}>`)}

View File

@ -42,6 +42,23 @@ function displayButton (dbo: displayButtonOptions): void {
if ('href' in dbo) {
button.href = dbo.href
}
if (!button.href || button.href === '#') {
// No href => it is not a link.
button.role = 'button'
button.tabIndex = 0
// We must also ensure that the enter key is triggering the onclick
if (button.onclick) {
button.onkeydown = ev => {
if (ev.key === 'Enter') {
ev.preventDefault()
button.click()
}
}
}
}
if (('targetBlank' in dbo) && dbo.targetBlank) {
button.target = '_blank'
}
@ -52,6 +69,10 @@ function displayButton (dbo: displayButtonOptions): void {
tmp.innerHTML = svg.trim()
const svgDom = tmp.firstChild
if (svgDom) {
if ('ariaHidden' in (svgDom as HTMLElement)) {
// Icon must be hidden for screen readers.
(svgDom as HTMLElement).ariaHidden = 'true'
}
button.prepend(svgDom)
}
} catch (err) {

View File

@ -16,8 +16,6 @@ import { localizedHelpUrl } from '../../utils/help'
import { getBaseRoute } from '../../utils/uri'
import { displayConverseJS } from '../../utils/conversejs'
let savedMyPluginFlexGrow: string | undefined
/**
* Initialize the chat for the current video
* @param video the video
@ -25,7 +23,6 @@ let savedMyPluginFlexGrow: string | undefined
async function initChat (video: Video): Promise<void> {
const ptContext = getPtContext()
const logger = ptContext.logger
savedMyPluginFlexGrow = undefined
if (!video) {
logger.error('No video provided')
@ -46,6 +43,8 @@ async function initChat (video: Video): Promise<void> {
container.setAttribute('id', 'peertube-plugin-livechat-container')
container.setAttribute('peertube-plugin-livechat-state', 'initializing')
container.setAttribute('peertube-plugin-livechat-current-url', window.location.href)
container.role = 'region'
container.ariaLabel = await ptContext.ptOptions.peertubeHelpers.translate(LOC_CHAT)
placeholder.append(container)
try {
@ -353,19 +352,6 @@ function _hackStyles (on: boolean): void {
buttons.classList.remove('peertube-plugin-livechat-buttons-open')
}
})
const myPluginPlaceholder: HTMLElement | null = document.querySelector('my-plugin-placeholder')
if (on) {
// Saving current style attributes and maximazing space for the chat
if (myPluginPlaceholder) {
savedMyPluginFlexGrow = myPluginPlaceholder.style.flexGrow // Should be "", but can be anything else.
myPluginPlaceholder.style.flexGrow = '1'
}
} else {
// restoring values...
if (savedMyPluginFlexGrow !== undefined && myPluginPlaceholder) {
myPluginPlaceholder.style.flexGrow = savedMyPluginFlexGrow
}
}
} catch (err) {
getPtContext().logger.error(`Failed hacking styles: '${err as string}'`)
}

View File

@ -167,7 +167,7 @@ async function displayConverseJS (
const converseJSParams: InitConverseJSParams = await (response).json()
if (!pollListenerInitiliazed) {
// First time we got here, initiliaze this event:
// First time we got here, initialize this event:
const i18nVoteOk = await clientOptions.peertubeHelpers.translate(LOC_POLL_VOTE_OK)
pollListenerInitiliazed = true
document.addEventListener('livechat-poll-vote', () => {

View File

@ -15,32 +15,22 @@ set -x
# Set CONVERSE_VERSION and CONVERSE_REPO to select which repo and tag/commit/branch use.
# Defaults values:
CONVERSE_VERSION="v10.1.6"
CONVERSE_VERSION="v11.0.0"
CONVERSE_REPO="https://github.com/conversejs/converse.js.git"
# You can eventually set CONVERSE_COMMIT to a specific commit ID, if you want to apply some patches.
CONVERSE_COMMIT=""
# 2024-09-02: using Converse upstream (v11 WIP).
CONVERSE_COMMIT="9952046d580bc2930e29833f4c9987a3d4c95bc2"
# 2014-01-16: we are using a custom version, to wait for some PR to be apply upstream.
# This version includes following changes:
# - #converse.js/3300: Adding the maxWait option for `debouncedPruneHistory`
# - #converse.js/3302: debounce MUC sidebar rendering
# - Fix: refresh the MUC sidebar when participants collection is sorted
# - Fix: MUC occupant list does not sort itself on nicknames or roles changes
# - Fix inconsistency between browsers on textarea outlines
# - Fix: room information not correctly refreshed when modifications are made by other users
# This version already includes following changes that will not be merged in ConverseJS upstream:
# - Don't load vCards for all room occupants when the right menu is closed
# - Changing the default avatar, for something very light (to mitigate blinking effect when vCards are loaded)
# - Custom settings livechat_load_all_vcards for the readonly mode
# - Adding "users" icon in the menu toggle button
# - Removing unecessary plugins: headless/pubsub, minimize, notifications, profile, omemo, push, roomlist, dragresize.
# - Destroy room: remove the challenge, and the new JID
# - New config option [colorize_username](https://conversejs.org/docs/html/configuration.html#colorize_username)
# - New loadEmojis hook, to customize emojis at runtime.
# - Fix custom emojis path when assets_path is not the default path.
CONVERSE_VERSION="livechat-10.1.0"
# CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
# It is possible to use another repository, if we want some customization that are not upstream (yet):
# CONVERSE_VERSION="livechat"
# # CONVERSE_COMMIT="4402fcc3fc60f6c9334f86528c33a0b463371d12"
# CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
# CONVERSE_COMMIT="xxxx"
# 2024-09-03: include badges short label and quick fix for sendMessage button
CONVERSE_REPO="https://github.com/JohnXLivingston/converse.js"
CONVERSE_VERSION="livechat-11.0.1"
CONVERSE_COMMIT=""
rootdir="$(pwd)"
src_dir="$rootdir/conversejs"

View File

@ -34,6 +34,7 @@ declare global {
env: {
html: Function
sizzle: Function
dayjs: Function
}
}
initConversePlugins: typeof initConversePlugins
@ -218,20 +219,24 @@ async function initConverse (
// * mode === chat-only + !transparent + !readonly + is using a livechat token
// Technically it would work in 'chat-only' mode, but i don't want to add too many things to test
// (and i now there is some CSS bugs in the task list).
let enableTask = false
// Same for the moderator notes app.
let enableApps = false
if (chatIncludeMode === 'peertube-video' || chatIncludeMode === 'peertube-fullpage') {
enableTask = true
enableApps = true
} else if (
chatIncludeMode === 'chat-only' &&
usedLivechatToken &&
!initConverseParams.transparent &&
!initConverseParams.forceReadonly
) {
enableTask = true
enableApps = true
}
if (enableTask) {
if (enableApps) {
params.livechat_task_app_enabled = true
params.livechat_task_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
params.livechat_note_app_enabled = true
params.livechat_note_app_restore = chatIncludeMode === 'peertube-fullpage' || chatIncludeMode === 'chat-only'
params.livechat_mam_search_app_enabled = true
}
try {

View File

@ -8,14 +8,13 @@
* @description This files will override the original ConverseJS index.js file.
*/
import '@converse/headless'
import 'shared/styles/index.scss'
import './i18n/index.js'
import 'shared/registry.js'
import { CustomElement } from 'shared/components/element'
import { VIEW_PLUGINS } from './shared/constants.js'
import { _converse, converse } from '@converse/headless/core'
import 'shared/styles/index.scss'
import { _converse, converse } from '@converse/headless'
/* START: Removable plugins
* ------------------------
@ -45,11 +44,16 @@ import './plugins/singleton/index.js'
import './plugins/fullscreen/index.js'
import '../custom/plugins/size/index.js'
import '../custom/plugins/mam-search/index.js'
import '../custom/plugins/notes/index.js'
import '../custom/plugins/tasks/index.js'
import '../custom/plugins/terms/index.js'
import '../custom/plugins/poll/index.js'
/* END: Removable components */
// Running some specific livechat patches:
import '../custom/livechat-patch-vcard.js'
import { CORE_PLUGINS } from './headless/shared/constants.js'
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
// We must add our custom plugins to CORE_PLUGINS (so it is white listed):
@ -57,11 +61,13 @@ CORE_PLUGINS.push('livechat-converse-size')
CORE_PLUGINS.push('livechat-converse-tasks')
CORE_PLUGINS.push('livechat-converse-terms')
CORE_PLUGINS.push('livechat-converse-poll')
CORE_PLUGINS.push('livechat-converse-notes')
CORE_PLUGINS.push('livechat-converse-mam-search')
// We must also add our custom ROOM_FEATURES, so that they correctly resets
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
_converse.CustomElement = CustomElement
_converse.exports.CustomElement = CustomElement
const initialize = converse.initialize

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless/core.js'
import { api } from '@converse/headless/index.js'
import { CustomElement } from 'shared/components/element.js'
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
import { __ } from 'i18n'

View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
// Here we are patching the vCard plugin, to add some specific optimizations.
import { _converse, api } from '@converse/headless/index.js'
import {
onOccupantAvatarChanged,
setVCardOnModel,
setVCardOnOccupant
} from '@converse/headless/plugins/vcard/utils.js'
const pluginDefinition = _converse.pluggable.plugins['converse-vcard']
const originalInitialize = pluginDefinition.initialize
pluginDefinition.initialize = function initialize () {
const previousListeners = _converse._events.chatRoomInitialized ?? []
originalInitialize.apply(this)
_converse.api.settings.extend({
livechat_load_all_vcards: false
})
// Now we must detect the new chatRoomInitialized listener, and remove it:
const listenersToRemove = []
for (const def of _converse._events.chatRoomInitialized ?? []) {
if (def.callback && !previousListeners.includes(def.callback)) {
listenersToRemove.push(def.callback)
}
}
for (const callback of listenersToRemove) {
console.debug('Livechat patching vcard: we must remove this listener', callback)
api.listen.not('chatRoomInitialized', callback)
}
// Adding the new listener:
api.listen.on('chatRoomInitialized', (m) => {
console.debug('Patched version of the vcard chatRoomInitialized event.')
setVCardOnModel(m)
// loadAll: when in readonly mode (ie: OBS integration), always load all avatars.
const loadAll = api.settings.get('livechat_load_all_vcards') === true
let hiddenOccupants = m.get('hidden_occupants')
if (hiddenOccupants !== true || loadAll) {
m.occupants.forEach(setVCardOnOccupant)
}
m.listenTo(m.occupants, 'add', (occupant) => {
if (hiddenOccupants !== true || loadAll) {
setVCardOnOccupant(occupant)
}
})
m.on('change:hidden_occupants', () => {
hiddenOccupants = m.get('hidden_occupants')
if (hiddenOccupants !== true || loadAll) {
m.occupants.forEach(setVCardOnOccupant)
}
})
m.listenTo(m.occupants, 'change:image_hash', o => onOccupantAvatarChanged(o))
})
}

View File

@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api, converse } from '../../../src/headless/index.js'
import { XMLNS_MAM_SEARCH } from './constants.js'
const env = converse.env
const {
$iq,
Strophe,
sizzle,
log,
TimeoutError,
__,
u
} = env
const NS = Strophe.NS
async function query (options) {
if (!api.connection.connected()) {
throw new Error('Can\'t call `api.livechat_mam_search.query` before having established an XMPP session')
}
if (!options?.room) {
throw new Error('api.livechat_mam_search.query: Missing room parameter.')
}
const attrs = {
type: 'set',
to: options.room
}
const jid = attrs.to
const supported = await api.disco.supports(XMLNS_MAM_SEARCH, jid)
if (!supported) {
log.warn(`Did not search MAM archive for ${jid} because it doesn't support ${XMLNS_MAM_SEARCH}`)
return { messages: [] }
}
const queryid = u.getUniqueId()
const stanza = $iq(attrs).c('query', { xmlns: XMLNS_MAM_SEARCH, queryid: queryid })
stanza.c('x', { xmlns: NS.XFORM, type: 'submit' })
.c('field', { var: 'FORM_TYPE', type: 'hidden' })
.c('value').t(XMLNS_MAM_SEARCH).up().up()
if (options.from) {
stanza.c('field', { var: 'from' }).c('value')
.t(options.from).up().up()
}
if (options.occupant_id) {
stanza.c('field', { var: 'occupant_id' }).c('value')
.t(options.occupant_id).up().up()
}
stanza.up()
// TODO: handle RSM (pagination.)
const connection = api.connection.get()
const messages = []
const messageHandler = connection.addHandler((stanza) => {
const result = sizzle(`message > result[xmlns="${NS.MAM}"]`, stanza).pop()
if (result === undefined || result.getAttribute('queryid') !== queryid) {
return true
}
const from = stanza.getAttribute('from')
if (from !== attrs.to) {
log.warn(`Ignoring alleged groupchat MAM message from ${from}`)
return true
}
messages.push(stanza)
return true
}, NS.MAM)
let error
const timeout = api.settings.get('message_archiving_timeout')
const iqResult = await api.sendIQ(stanza, timeout, false)
if (iqResult === null) {
const errMsg = __('Timeout while trying to fetch archived messages.')
log.error(errMsg)
error = new TimeoutError(errMsg)
return { messages, error }
} else if (u.isErrorStanza(iqResult)) {
const errMsg = __('An error occurred while querying for archived messages.')
log.error(errMsg)
log.error(iqResult)
error = new Error(errMsg)
return { messages, error }
}
connection.deleteHandler(messageHandler)
return { messages }
}
async function showMessagesFrom (occupant) {
const appElement = document.querySelector('livechat-converse-muc-mam-search-app')
if (!appElement) {
throw new Error('Cant find Search App Element')
}
appElement.searchFrom(occupant)
await appElement.showApp()
await appElement.updateComplete // waiting for the app to be open
return appElement
}
export default {
query,
showMessagesFrom
}

View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless'
import { parseMUCMessage } from '@converse/headless/plugins/muc/parsers.js'
import { MUCApp } from '../../../shared/components/muc-app/index.js'
import { tplMamSearchApp } from '../templates/muc-mam-search-app.js'
/**
* Custom Element to display the Mam Search Application.
*/
export default class MUCMamSearchApp extends MUCApp {
restoreSettingName = undefined
sessionStorageRestoreKey = undefined
static get properties () {
return {
model: { type: Object, attribute: true }, // the muc model
occupant: { type: Object, attribute: true }, // the occupant to search (can be undefined if no current search)
results: { type: Object, attribute: true } // a Collection with the results.
}
}
render () {
return tplMamSearchApp(this, this.model, this.occupant)
}
searchFrom (occupant) {
this.results = undefined
this.occupant = occupant
const p = api.livechat_mam_search.query({
room: this.model.get('jid'),
// FIXME: shouldn't we escape the nick? cant see any code that escapes it in Converse.
from: occupant.get('from') || this.model.get('jid') + '/' + (occupant.get('nick') ?? ''),
occupant_id: occupant.get('occupant_id')
})
// don't wait the result to show something! (there will be a spinner)
p.then(async (results) => {
this.occupant = occupant // in case user did simultaneous requests
const messages = await Promise.all(results.messages.map(s => parseMUCMessage(s, this.model)))
// Note: we are not using MUCMessage objects, because we don't want the objects
// used here to interract with objects in the chat rooms.
// We could have a lot of unwanted sideeffects.
this.results = messages.reverse()
})
}
}
api.elements.define('livechat-converse-muc-mam-search-app', MUCMamSearchApp)

View File

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { tplMucMamSearchMessage } from '../templates/muc-mam-search-message.js'
import { api } from '@converse/headless'
import '../styles/muc-mam-search-message.scss'
export default class MUCMamSearchMessageView extends CustomElement {
static get properties () {
return {
message: { type: Object, attribute: true }, // /!\ this is not a model
mucModel: { type: Object, attribute: true },
searchOccupantModel: { type: Object, attribute: true }
}
}
async initialize () {
this.listenTo(this.mucModel, 'change', () => this.requestUpdate())
this.listenTo(this.searchOccupantModel, 'change', () => this.requestUpdate())
}
render () {
return tplMucMamSearchMessage(this, this.mucModel, this.searchOccupantModel, this.message)
}
getMessageOccupant () {
const occupants = this.mucModel?.occupants
if (!occupants?.findOccupant) { return undefined }
const nick = this.message.nick
const jid = this.message.from
const occupantId = this.message.occupant_id
if (!nick && !jid && !occupantId) {
return undefined
}
if (occupantId) {
const o = occupants.findOccupant({ occupant_id: occupantId })
if (o) {
return o
}
}
if (jid) {
const o = occupants.findOccupant({
jid,
nick
})
if (o) {
return o
}
}
// If we don't find it, maybe it is a user that has spoken a long time ago (or never spoked).
// In such case, we must create a dummy occupant:
const o = occupants.create({
nick,
occupant_id: occupantId,
jid
})
return o
}
getDateTime () {
if (!this.message.time) {
return undefined
}
try {
const d = new Date(this.message.time)
return d.toLocaleDateString() + ' - ' + d.toLocaleTimeString()
} catch (err) {
console.log(err)
return undefined
}
}
}
api.elements.define('livechat-converse-muc-mam-search-message', MUCMamSearchMessageView)

View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { tplMucMamSearchOccupant } from '../templates/muc-mam-search-occupant'
import { api } from '@converse/headless'
import '../styles/muc-mam-search-occupant.scss'
export default class MUCMamSearchOccupantView extends CustomElement {
static get properties () {
return {
model: { type: Object, attribute: true },
message: { type: Object, attribute: true } // optional message.
}
}
async initialize () {
this.listenTo(this.model, 'change', () => this.requestUpdate())
}
render () {
return tplMucMamSearchOccupant(this, this.model, this.message)
}
}
api.elements.define('livechat-converse-muc-mam-search-occupant', MUCMamSearchOccupantView)

View File

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
export const XMLNS_MAM_SEARCH = 'urn:xmpp:mam:2#x-search'

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api, converse } from '../../../src/headless/index.js'
import { getMessageActionButtons, getOccupantActionButtons } from './utils.js'
import mamSearchApi from './api.js'
import './components/muc-mam-search-app-view.js'
import './components/muc-mam-search-occupant-view.js'
import './components/muc-mam-search-message-view.js'
converse.plugins.add('livechat-converse-mam-search', {
dependencies: ['converse-muc', 'converse-muc-views'],
async initialize () {
const _converse = this._converse
Object.assign(api, {
livechat_mam_search: mamSearchApi
})
_converse.api.settings.extend({
livechat_mam_search_app_enabled: false
})
// Adding buttons on messages:
_converse.api.listen.on('getMessageActionButtons', getMessageActionButtons)
// Adding buttons on occupants:
_converse.api.listen.on('getOccupantActionButtons', getOccupantActionButtons)
// FIXME: should we listen to any event (feature/affiliation change?, mam_enabled?) to refresh messageActionButtons?
}
})

View File

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
livechat-converse-muc-mam-search-message {
border: 1px solid var(--chatroom-head-bg-color);
border-radius: 4px;
display: block;
margin: 0.25em 0;
padding: 0.25em;
width: 100%;
converse-rich-text {
color: var(--message-text-color);
font-size: var(--message-font-size);
padding: 0;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
}
.livechat-message-date {
font-size: 0.75em;
list-style: none;
text-align: right;
}
}
}

View File

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
livechat-converse-muc-mam-search-occupant {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
padding: 0.25em;
& > a {
display: flex;
flex-flow: row nowrap;
align-items: center;
span {
font-weight: bold;
margin-left: 0.5em;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
}
& > ul {
font-weight: lighter;
font-size: 0.75em;
list-style: none;
text-align: right;
}
}
}

View File

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n'
function tplContent (el, mucModel, occupantModel) {
return html`
${
occupantModel
? html`
<livechat-converse-muc-mam-search-occupant
.model=${occupantModel}
></livechat-converse-muc-mam-search-occupant>
`
: ''
}
<hr>
${
el.results
? repeat(el.results, (message) => message.id, message => {
return html`<livechat-converse-muc-mam-search-message
.message=${message} .mucModel=${mucModel} .searchOccupantModel=${occupantModel}
></livechat-converse-muc-mam-search-message>`
})
: html`<livechat-spinner></livechat-spinner>`
}
`
}
export function tplMamSearchApp (el, mucModel, occupantModel) {
if (!mucModel) {
// should not happen
return html``
}
if (!el.show) {
return html``
}
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_message_search)
// eslint-disable-next-line no-undef
const i18nHelp = __(LOC_online_help)
const helpUrl = converseLocalizedHelpUrl({
page: 'documentation/user/streamers/moderation'
})
return tplMUCApp(
el,
i18nSearch,
helpUrl,
i18nHelp,
tplContent(el, mucModel, occupantModel)
)
}

View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
/**
* Renders the message as a search result.
* @param el The message element
* @param mucModel The MUC model
* @param searchOccupantModel The model of the occupant for which we are searching
* @param message The message (warning: this is not a model)
* @returns TemplateResult (or equivalent)
*/
export function tplMucMamSearchMessage (el, mucModel, searchOccupantModel, message) {
const occupant = el.getMessageOccupant()
return html`
${
occupant
? html`
<livechat-converse-muc-mam-search-occupant
.model=${occupant}
.message=${message}
></livechat-converse-muc-mam-search-occupant>`
: ''
}
<converse-rich-text
render_styling
text=${message.body}>
</converse-rich-text>
<div class="livechat-message-date">${el.getDateTime()}</div>`
}

View File

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
import { api } from '@converse/headless'
import { getAuthorStyle } from '../../../../src/utils/color.js'
import { __ } from 'i18n'
export function tplMucMamSearchOccupant (el, occupant, message) {
const authorStyle = getAuthorStyle(occupant)
const jid = occupant.get('jid')
const occupantId = occupant.get('occupant_id')
return html`
<a @click=${(ev) => {
api.modal.show('converse-muc-occupant-modal', { model: occupant }, ev)
}}>
<converse-avatar
.model=${occupant}
class="avatar chat-msg__avatar"
name="${occupant.getDisplayName()}"
nonce=${occupant.vcard?.get('vcard_updated')}
height="30" width="30"></converse-avatar>
<span style=${authorStyle}>${occupant.getDisplayName()}</span>
</a>
<ul aria-hidden="true">
${
// user changed nick: display the original nick
message && message.nick !== undefined && message.nick !== occupant.get('nick')
// eslint-disable-next-line no-undef
? html`<li title=${__(LOC_message_search_original_nick)}>${message.nick}</li>`
: ''
}
${jid ? html`<li title=${__('XMPP Address')}>${jid}</li>` : ''}
${occupantId ? html`<li title=${__('Occupant Id')}>${occupantId}</li>` : ''}
</ul>`
}

View File

@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '../../../src/headless/index.js'
import { XMLNS_MAM_SEARCH } from './constants.js'
import { __ } from 'i18n'
function getMessageActionButtons (messageActionsEl, buttons) {
const messageModel = messageActionsEl.model
if (!api.settings.get('livechat_mam_search_app_enabled')) {
return buttons
}
if (messageModel.get('type') !== 'groupchat') {
// only on groupchat message.
return buttons
}
if (!messageModel.occupant) {
return buttons
}
const muc = messageModel.collection?.chatbox
if (!muc) {
return buttons
}
if (!muc.features?.get?.(XMLNS_MAM_SEARCH)) {
return buttons
}
const myself = muc.getOwnOccupant()
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
return buttons
}
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_search_occupant_message)
buttons.push({
i18n_text: i18nSearch,
handler: async (ev) => {
ev.preventDefault()
api.livechat_mam_search.showMessagesFrom(messageModel.occupant)
},
button_class: '',
icon_class: 'fa fa-magnifying-glass',
name: 'muc-mam-search'
})
return buttons
}
function getOccupantActionButtons (occupant, buttons) {
if (!api.settings.get('livechat_mam_search_app_enabled')) {
return buttons
}
const muc = occupant.collection?.chatroom
if (!muc) {
return buttons
}
if (!muc.features?.get?.(XMLNS_MAM_SEARCH)) {
return buttons
}
const myself = muc.getOwnOccupant()
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
return buttons
}
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_search_occupant_message)
buttons.push({
i18n_text: i18nSearch,
handler: async (ev) => {
ev.preventDefault()
api.livechat_mam_search.showMessagesFrom(occupant)
},
button_class: '',
icon_class: 'fa fa-magnifying-glass',
name: 'muc-mam-search'
})
return buttons
}
export {
getMessageActionButtons,
getOccupantActionButtons
}

View File

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
async function openNotes () {
const appElement = document.querySelector('livechat-converse-muc-note-app')
if (!appElement) {
throw new Error('Cant find Note App Element')
}
await appElement.showApp()
await appElement.updateComplete // waiting for the app to be open
const notesElement = appElement.querySelector('livechat-converse-muc-notes')
if (!notesElement) {
throw new Error('Cant find Notes Element')
}
await notesElement.updateComplete
return notesElement
}
async function openCreateNoteForm (occupant) {
const notesElement = await openNotes()
await notesElement.openCreateNoteForm(undefined, occupant)
}
async function searchNotesAbout (occupant) {
const notesElement = await openNotes()
await notesElement.filterNotes({ occupant })
}
export default {
openNotes,
openCreateNoteForm,
searchNotesAbout
}

View File

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless'
import { MUCApp } from '../../../shared/components/muc-app/index.js'
import { tplMUCNoteApp } from '../templates/muc-note-app.js'
/**
* Custom Element to display the Notes Application.
*/
export default class MUCNoteApp extends MUCApp {
restoreSettingName = 'livechat_note_app_restore'
sessionStorageRestoreKey = 'livechat-converse-note-app-show'
render () {
return tplMUCNoteApp(this, this.model)
}
}
api.elements.define('livechat-converse-muc-note-app', MUCNoteApp)

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { tplMucNoteOccupant } from '../templates/muc-note-occupant'
import { api } from '@converse/headless'
import '../styles/muc-note-occupant.scss'
export default class MUCNoteOccupantView extends CustomElement {
static get properties () {
return {
model: { type: Object, attribute: true },
note: { type: Object, attribute: true }, // optional associated note
full_display: { type: Boolean, attribute: true }
}
}
async initialize () {
this.listenTo(this.model, 'change', () => this.requestUpdate())
}
render () {
return tplMucNoteOccupant(this, this.model, this.note)
}
}
api.elements.define('livechat-converse-muc-note-occupant', MUCNoteOccupantView)

View File

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless'
import { tplMucNote } from '../templates/muc-note'
import { __ } from 'i18n'
import '../styles/muc-note.scss'
export default class MUCNoteView extends CustomElement {
static get properties () {
return {
model: { type: Object, attribute: true },
edit: { type: Boolean, attribute: false },
is_ocupant_filter: { type: Boolean, attribute: true }
}
}
async initialize () {
this.edit = false
if (!this.model) {
return
}
this.listenTo(this.model, 'change', () => this.requestUpdate())
}
render () {
return tplMucNote(this, this.model)
}
shouldUpdate (changedProperties) {
if (!super.shouldUpdate(...arguments)) { return false }
// When a note is currently edited, and another users change the order,
// it could refresh losing the current form.
// To avoid this, we cancel update here.
// Note: of course, if 'edit' is part of the edited properties, we must update anyway
// (it means we just leaved the form)
if (this.edit && !changedProperties.has('edit')) {
console.info('Canceling an update on note, because it is currently edited', this)
return false
}
return true
}
async saveNote (ev) {
ev?.preventDefault?.()
const description = ev.target.description.value
if ((description ?? '') === '') { return }
try {
this.querySelectorAll('input[type=submit]').forEach(el => {
el.setAttribute('disabled', true)
el.classList.add('disabled')
})
const note = this.model
note.set('description', description)
await note.saveItem()
this.edit = false
this.requestUpdate() // In case we cancel another update in shouldUpdate
} catch (err) {
console.error(err)
} finally {
this.querySelectorAll('input[type=submit]').forEach(el => {
el.removeAttribute('disabled')
el.classList.remove('disabled')
})
}
}
async deleteNote (ev) {
ev?.preventDefault?.()
// eslint-disable-next-line no-undef
const i18nConfirmDelete = __(LOC_moderator_note_delete_confirm)
const result = await api.confirm(i18nConfirmDelete)
if (!result) { return }
try {
await this.model.deleteItem()
} catch (err) {
api.alert(
'error', __('Error'), [__('Error')]
)
}
}
async toggleEdit () {
this.edit = !this.edit
if (this.edit) {
await this.updateComplete
const textarea = this.querySelector('textarea[name="description"]')
if (textarea) {
textarea.focus()
// Placing cursor at the end:
textarea.selectionStart = textarea.value.length
textarea.selectionEnd = textarea.selectionStart
}
}
}
}
api.elements.define('livechat-converse-muc-note', MUCNoteView)

View File

@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless'
import tplMucNotes from '../templates/muc-notes'
import { __ } from 'i18n'
import { DraggablesCustomElement } from '../../../shared/components/draggables/index.js'
import '../styles/muc-notes.scss'
export default class MUCNotesView extends DraggablesCustomElement {
static get properties () {
return {
model: { type: Object, attribute: true },
create_note_error_message: { type: String, attribute: false },
create_note_opened: { type: Boolean, attribute: false },
create_note_about_occupant: { type: Object, attribute: false },
occupant_filter: { type: Object, attribute: false }
}
}
async initialize () {
this.create_note_error_message = ''
if (!this.model) {
return
}
this.draggableTagName = 'livechat-converse-muc-note'
this.droppableTagNames = ['livechat-converse-muc-note']
this.droppableAlwaysBottomTagNames = []
// Adding or removing a new note: we must update.
this.listenTo(this.model, 'add', () => this.requestUpdate())
this.listenTo(this.model, 'remove', () => this.requestUpdate())
this.listenTo(this.model, 'sort', () => this.requestUpdate())
await super.initialize()
}
render () {
return tplMucNotes(this, this.model)
}
async openCreateNoteForm (ev, occupant) {
ev?.preventDefault?.()
this.create_note_opened = true
this.create_note_about_occupant = occupant ?? undefined
if (this.create_note_about_occupant === undefined && this.occupant_filter) {
// if we have a current filter, we can use it for the new note.
this.create_note_about_occupant = this.occupant_filter
}
await this.updateComplete
const textarea = this.querySelector('.notes-create-note textarea[name="description"]')
if (textarea) {
textarea.focus()
}
}
closeCreateNoteForm (ev) {
ev?.preventDefault?.()
this.create_note_opened = false
this.create_note_about_occupant = undefined
}
filterNotes (filters) {
this.occupant_filter = filters?.occupant || undefined
}
async submitCreateNote (ev) {
ev.preventDefault()
const description = ev.target.description.value
if (this.create_note_error_message) {
this.create_note_error_message = ''
}
if ((description ?? '') === '') { return }
try {
this.querySelectorAll('input[type=submit]').forEach(el => {
el.setAttribute('disabled', true)
el.classList.add('disabled')
})
await this.model.createNote({
description: description,
about_jid: ev.target.about_jid?.value || undefined,
about_nick: ev.target.about_nick?.value || undefined,
about_occupant_id: ev.target.about_occupant_id?.value || undefined
})
this.closeCreateNoteForm()
} catch (err) {
console.error(err)
// eslint-disable-next-line no-undef
this.create_note_error_message = __(LOC_moderator_notes_create_error)
} finally {
this.querySelectorAll('input[type=submit]').forEach(el => {
el.removeAttribute('disabled')
el.classList.remove('disabled')
})
}
}
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
super._dropDone(...arguments)
console.log('[livechat note drag&drop] Note dropped...')
const note = draggedEl.model
if (!note) {
throw new Error('No model for the draggedEl')
}
const targetNote = droppedOnEl.model
if (!targetNote) {
throw new Error('No model for the droppedOnEl')
}
if (note === targetNote) {
console.log('[livechat note drag&drop] Note dropped on itself, nothing to do')
return
}
let newOrder = targetNote.get('order') ?? 0
if (onTopHalf) { newOrder = Math.max(0, newOrder + 1) } // reverse order!
// Warning: the order of the collection is reversed!
// _saveOrders needs it in ascending order!
this._saveOrders(Array.from(this.model).reverse(), note, newOrder)
}
}
api.elements.define('livechat-converse-muc-notes', MUCNotesView)

View File

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
export const XMLNS_NOTE = 'urn:peertube-plugin-livechat:note'

View File

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse } from '../../../src/headless/index.js'
import { XMLNS_NOTE } from './constants.js'
import { ChatRoomNote } from './note.js'
import { ChatRoomNotes } from './notes.js'
import {
initOrDestroyChatRoomNotes, getHeadingButtons, getMessageActionButtons, getOccupantActionButtons
} from './utils.js'
import notesApi from './api.js'
import './components/muc-note-app-view.js'
import './components/muc-notes-view.js'
import './components/muc-note-view.js'
import './components/muc-note-occupant-view.js'
converse.plugins.add('livechat-converse-notes', {
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
initialize () {
Object.assign(
_converse.exports,
{
ChatRoomNotes,
ChatRoomNote
}
)
_converse.api.settings.extend({
livechat_note_app_enabled: false,
livechat_note_app_restore: false // should we open the app by default if it was previously oppened?
})
Object.assign(_converse.api, {
livechat_notes: notesApi
})
_converse.api.listen.on('chatRoomInitialized', muc => {
muc.session.on('change:connection_status', _session => {
// When joining a room, initializing the Notes object (if user has access),
// When disconnected from a room, destroying the Notes object:
initOrDestroyChatRoomNotes(muc)
})
// When the current user affiliation changes, we must also delete or initialize the TaskLists object:
muc.occupants.on('change:affiliation', occupant => {
if (occupant.get('jid') !== _converse.bare_jid) { // only for myself
return
}
initOrDestroyChatRoomNotes(muc)
})
// To be sure that everything works in any case, we also must listen for addition in muc.features.
muc.features.on('change:' + XMLNS_NOTE, () => {
initOrDestroyChatRoomNotes(muc)
})
})
// adding the "Notes" button in the MUC heading buttons:
_converse.api.listen.on('getHeadingButtons', getHeadingButtons)
// Adding buttons on messages:
_converse.api.listen.on('getMessageActionButtons', getMessageActionButtons)
// Adding buttons on occupants:
_converse.api.listen.on('getOccupantActionButtons', getOccupantActionButtons)
}
})

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { PubSubManager } from '../../shared/lib/pubsub-manager.js'
export class NotePubSubManager extends PubSubManager {
_additionalModelToData (item, data) {
super._additionalModelToData(item, data)
data.about_jid = item.get('about_jid')
data.about_occupant_id = item.get('about_occupant_id')
data.about_nick = item.get('about_nick')
}
_additionalDataToItemNode (data, item) {
super._additionalDataToItemNode(data, item)
const aboutAttributes = {}
if (data.about_jid !== undefined) {
aboutAttributes.jid = data.about_jid
}
if (data.about_nick !== undefined) {
aboutAttributes.nick = data.about_nick
}
const occupantId = data.about_occupant_id
if (occupantId !== undefined || Object.values(aboutAttributes).length) {
item.c('note-about', aboutAttributes)
if (occupantId) {
item.c('occupant-id', { xmlns: 'urn:xmpp:occupant-id:0', id: occupantId }).up()
}
item.up()
}
}
_additionalParseItemNode (itemNode, type, data) {
super._additionalParseItemNode(itemNode, type, data)
const about = itemNode.querySelector('& > note-about')
if (!about) { return }
data.about_jid = about.getAttribute('jid')
data.about_nick = about.getAttribute('nick')
const occupantIdEl = about.querySelector('& > occupant-id')
if (occupantIdEl) {
data.about_occupant_id = occupantIdEl.getAttribute('id')
}
}
}

View File

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { Model } from '@converse/skeletor/src/model.js'
/**
* A chat room note.
* @class
* @namespace _converse.exports.ChatRoomNote
* @memberof _converse
*/
class ChatRoomNote extends Model {
idAttribute = 'id'
_aboutOccupantCache = null
_aboutOccupantCacheFor = null
async saveItem () {
console.log('Saving note ' + this.get('id') + '...')
await this.collection.chatroom.noteManager.saveItem(this)
console.log('Note ' + this.get('id') + ' saved.')
}
async deleteItem () {
return this.collection.chatroom.noteManager.deleteItems([this])
}
getAboutOccupant () {
const occupants = this.collection.chatroom?.occupants
if (!occupants?.findOccupant) { return undefined }
const nick = this.get('about_nick')
const jid = this.get('about_jid')
const occupantId = this.get('about_occupant_id')
if (!nick && !jid && !occupantId) {
this._aboutOccupantCache = null
this._aboutOccupantCacheFor = null
return undefined
}
// Keeping some cache, to avoid intensive search on each rendering.
const cacheKey = `${occupantId ?? ''} ${jid ?? ''} ${nick ?? ''}`
if (this._aboutOccupantCacheFor === cacheKey && this._aboutOccupantCache) {
return this._aboutOccupantCache
}
this._aboutOccupantCacheFor = cacheKey
if (occupantId) {
const o = occupants.findOccupant({ occupant_id: occupantId })
if (o) {
this._aboutOccupantCache = o
return o
}
}
if (jid) {
const o = occupants.findOccupant({
jid,
nick
})
if (o) {
this._aboutOccupantCache = o
return o
}
}
// If we don't find it, maybe it is a user that has spoken a long time ago (or never spoked).
// In such case, we must create a dummy occupant:
this._aboutOccupantCache = occupants.create({
nick,
occupant_id: occupantId,
jid
})
return this._aboutOccupantCache
}
}
export {
ChatRoomNote
}

View File

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { Collection } from '@converse/skeletor/src/collection.js'
import { ChatRoomNote } from './note'
import { initStorage } from '@converse/headless/utils/storage.js'
/**
* A list of {@link _converse.exports.ChatRoomNote} instances, representing notes associated to a MUC.
* @class
* @namespace _converse.exports.ChatRoomNotes
* @memberOf _converse
*/
class ChatRoomNotes extends Collection {
model = ChatRoomNote
initialize (models, options) {
this.model = ChatRoomNote // don't know why, must do it again here
super.initialize(arguments)
this.chatroom = options.chatroom
const id = `converse-livechat-notes-${this.chatroom.get('jid')}`
initStorage(this, id, 'session')
this.on('change:order', () => this.sort())
}
comparator (n1, n2) {
// must reverse order
const o1 = n1.get('order') ?? 0
const o2 = n2.get('order') ?? 0
return o1 < o2 ? 1 : o1 > o2 ? -1 : 0
}
async createNote (data) {
data = Object.assign({}, data)
if (!data.order) {
data.order = 1 + Math.max(
0,
...(this.map(n => n.get('order') ?? 0).filter(o => !isNaN(o)))
)
}
console.log('Creating note...')
await this.chatroom.noteManager.createItem(this, data)
console.log('Note created.')
}
}
export {
ChatRoomNotes
}

View File

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
livechat-converse-muc-note-occupant {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
padding: 0.25em;
& > a {
display: flex;
flex-flow: row nowrap;
align-items: center;
span {
font-weight: bold;
margin-left: 0.5em;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
}
& > ul {
font-weight: lighter;
font-size: 0.75em;
list-style: none;
text-align: right;
}
}
}

View File

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
livechat-converse-muc-note {
padding: 0;
width: 100%;
.note-line {
border: 1px solid var(--chatroom-head-bg-color);
border-radius: 4px;
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
margin: 0.25em 0;
padding: 0.25em;
column-gap: 0.25em;
width: 100%;
.note-content {
flex-grow: 2;
}
.note-description {
white-space: pre-wrap;
}
.note-action {
background: unset;
border: 0;
padding-left: 0.25em;
padding-right: 0.25em;
}
form {
width: 100%;
}
}
}
}

View File

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
.notes-actions {
display: flex;
flex-flow: row nowrap;
justify-content: right;
width: 100%;
}
.notes-action {
background: unset;
border: 0;
padding-left: 0.25em;
padding-right: 0.25em;
}
.notes-filters {
border: 1px solid var(--chatroom-head-bg-color);
border-radius: 4px;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
margin: 0.25em 0;
padding: 0.25em;
column-gap: 0.25em;
width: 100%;
livechat-converse-muc-note-occupant {
flex-grow: 2;
}
}
}

View File

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit'
import { __ } from 'i18n'
export function tplMUCNoteApp (el, mucModel) {
if (!mucModel) {
// should not happen
return html``
}
if (!mucModel.notes) {
// too soon, not initialized yet (this will happen)
return html``
}
if (!el.show) {
return html``
}
// eslint-disable-next-line no-undef
const i18nNotes = __(LOC_moderator_notes)
// eslint-disable-next-line no-undef
const i18nHelp = __(LOC_online_help)
const helpUrl = converseLocalizedHelpUrl({
page: 'documentation/user/streamers/moderation_notes'
})
return tplMUCApp(
el,
i18nNotes,
helpUrl,
i18nHelp,
html`<livechat-converse-muc-notes .model=${mucModel.notes}></livechat-converse-muc-notes>`
)
}

View File

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
import { api } from '@converse/headless'
import { getAuthorStyle } from '../../../../src/utils/color.js'
import { __ } from 'i18n'
export function tplMucNoteOccupant (el, occupant, note) {
const authorStyle = getAuthorStyle(occupant)
const jid = occupant.get('jid')
const occupantId = occupant.get('occupant_id')
return html`
<a @click=${(ev) => {
api.modal.show('converse-muc-occupant-modal', { model: occupant }, ev)
}}>
<converse-avatar
.model=${occupant}
class="avatar chat-msg__avatar"
name="${occupant.getDisplayName()}"
nonce=${occupant.vcard?.get('vcard_updated')}
height="30" width="30"></converse-avatar>
<span style=${authorStyle}>${occupant.getDisplayName()}</span>
</a>
${
el.full_display
? html`<ul aria-hidden="true">
${
// user changed nick: display the original nick
note && note.get('about_nick') && note.get('about_nick') !== occupant.get('nick')
// eslint-disable-next-line no-undef
? html`<li title=${__(LOC_moderator_note_original_nick)}>${note.get('about_nick')}</li>`
: ''
}
${jid ? html`<li title=${__('XMPP Address')}>${jid}</li>` : ''}
${occupantId ? html`<li title=${__('Occupant Id')}>${occupantId}</li>` : ''}
</ul>`
: ''
}
`
}

View File

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless'
import { html } from 'lit'
import { __ } from 'i18n'
export function tplMucNote (el, note) {
// eslint-disable-next-line no-undef
const i18nDelete = __(LOC_moderator_note_delete)
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_search_for_participant)
const aboutOccupant = note.getAboutOccupant()
return !el.edit
? html`
<div draggable="true" class="note-line draggables-line">
<div class="note-content">
${
aboutOccupant
? html`
<livechat-converse-muc-note-occupant
.full_display=${el.is_ocupant_filter}
.model=${aboutOccupant}
.note=${note}
></livechat-converse-muc-note-occupant>`
: ''
}
<div class="note-description">${note.get('description') ?? ''}</div>
</div>
${
aboutOccupant && el.is_ocupant_filter
? ''
: html`
<button type="button" class="note-action" @click=${ev => {
ev.preventDefault()
api.livechat_notes.searchNotesAbout(aboutOccupant)
}}>
<converse-icon class="fa fa-magnifying-glass" size="1em" title=${i18nSearch}></converse-icon>
</button>`
}
<button type="button" class="note-action" title="${__('Edit')}"
@click=${el.toggleEdit}
>
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
</button>
<button type="button" class="note-action" title="${i18nDelete}"
@click=${el.deleteNote}
>
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
</button>
</div>`
: html`
<div class="note-line draggables-line">
<form class="converse-form" @submit=${el.saveNote}>
${
aboutOccupant
? html`
<livechat-converse-muc-note-occupant
full_display=${true}
.model=${aboutOccupant}
.note=${note}
></livechat-converse-muc-note-occupant>
`
: ''
}
${_tplNoteForm(note)}
<fieldset>
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
<input type="button" class="btn btn-secondary button-cancel"
value="${__('Cancel')}" @click=${el.toggleEdit}
/>
</fieldset>
</form>
</div>`
}
function _tplNoteForm (note) {
// eslint-disable-next-line no-undef
const i18nNoteDesc = __(LOC_moderator_note_description)
return html`<fieldset>
<textarea
class="form-control" name="description"
placeholder="${i18nNoteDesc}"
>${note ? note.get('description') : ''}</textarea>
</fieldset>`
}
function _tplNoteOccupantFormFields (occupant) {
if (!occupant) { return '' }
return html`
<input type="hidden" name="about_nick" value=${occupant.get('nick')} />
<input type="hidden" name="about_jid" value=${occupant.get('jid')} />
<input type="hidden" name="about_occupant_id" value=${occupant.get('occupant_id')} />
`
}
export function tplMucCreateNoteForm (notesEl, occupant) {
const i18nOk = __('Ok')
const i18nCancel = __('Cancel')
return html`
<form class="notes-create-note converse-form" @submit=${notesEl.submitCreateNote}>
${
occupant
? html`
${_tplNoteOccupantFormFields(occupant)}
<livechat-converse-muc-note-occupant
full_display=${true}
.model=${occupant}
></livechat-converse-muc-note-occupant>
`
: ''
}
${_tplNoteForm(undefined)}
<fieldset>
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
<input type="button" class="btn btn-secondary button-cancel"
value="${i18nCancel}" @click=${notesEl.closeCreateNoteForm}
/>
${!notesEl.create_note_error_message
? ''
: html`<div class="invalid-feedback d-block">${notesEl.create_note_error_message}</div>`
}
</fieldset>
</form>`
}

View File

@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import { __ } from 'i18n'
import { tplMucCreateNoteForm } from './muc-note'
function tplFilters (el) {
const filterOccupant = el.occupant_filter
if (!filterOccupant) { return '' }
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_filters)
return html`
<div class="notes-filters">
<converse-icon class="fa fa-magnifying-glass" size="1em" title=${i18nSearch}></converse-icon>
${
filterOccupant
? html`<livechat-converse-muc-note-occupant
full_display=${true}
.model=${filterOccupant}
></livechat-converse-muc-note-occupant>`
: ''
}
<button type="button" class="notes-action" @click=${(ev) => {
ev?.preventDefault()
el.filterNotes({})
}} title="${__('Close')}">
<converse-icon class="fa fa-times" size="1em"></converse-icon>
</button>
</div>
<hr/>
`
}
function isFiltered (el, note) {
const filterOccupant = el.occupant_filter
if (!filterOccupant) { return false }
const noteOccupant = note.getAboutOccupant()
// there is an occupant filter, so if current note has no associated occupant, we can pass.
if (!noteOccupant) { return true }
if (noteOccupant === filterOccupant) {
// Yes!
return false
}
// We will also test for nickname, so that we can found anonymous users
// (they can have multiple associated occupants)
if (filterOccupant.get('nick') && filterOccupant.get('nick') === noteOccupant.get('nick')) {
return false
}
return true
}
export default function tplMucNotes (el, notes) {
if (!notes) { // if user loses rights
return html`` // FIXME: add a message like "you dont have access"?
}
return html`
${
el.create_note_opened ? tplMucCreateNoteForm(el, el.create_note_about_occupant) : tplCreateButton(el)
}
${tplFilters(el)}
${
repeat(notes, (note) => note.get('id'), (note) => {
return isFiltered(el, note)
? ''
: html`<livechat-converse-muc-note
.model=${note}
.is_ocupant_filter=${!!el.occupant_filter}
></livechat-converse-muc-note>`
})
}`
}
function tplCreateButton (el) {
// eslint-disable-next-line no-undef
const i18nCreateNote = __(LOC_moderator_note_create)
return html`
<div class="notes-actions">
<button type="button" class="notes-action" title="${i18nCreateNote}" @click=${el.openCreateNoteForm}>
<converse-icon class="fa fa-plus" size="1em"></converse-icon>
</button>
</div>`
}

View File

@ -0,0 +1,195 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { XMLNS_NOTE } from './constants.js'
import { NotePubSubManager } from './note-pubsub-manager.js'
import { converse, _converse, api } from '../../../src/headless/index.js'
import { __ } from 'i18n'
export function getHeadingButtons (view, buttons) {
const muc = view.model
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC.
return buttons
}
if (!muc.notes) { // this is defined only if user has access (see initOrDestroyChatRoomNotes)
return buttons
}
// Adding a "Open moderator noteds" button.
buttons.unshift({
// eslint-disable-next-line no-undef
i18n_text: __(LOC_moderator_notes),
handler: async (ev) => {
ev.preventDefault()
// opening or closing the muc notes:
const NoteAppEl = ev.target.closest('converse-root').querySelector('livechat-converse-muc-note-app')
NoteAppEl.toggleApp()
},
a_class: '',
icon_class: 'fa-note-sticky',
name: 'muc-notes'
})
return buttons
}
export function getMessageActionButtons (messageActionsEl, buttons) {
const messageModel = messageActionsEl.model
if (messageModel.get('type') !== 'groupchat') {
// only on groupchat message.
return buttons
}
if (!messageModel.occupant) {
return buttons
}
const muc = messageModel.collection?.chatbox
if (!muc?.notes) {
return buttons
}
// eslint-disable-next-line no-undef
const i18nCreate = __(LOC_moderator_note_create_for_participant)
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_search_for_participant)
buttons.push({
i18n_text: i18nCreate,
handler: async (ev) => {
ev.preventDefault()
await api.livechat_notes.openCreateNoteForm(messageModel.occupant)
},
button_class: '',
icon_class: 'fa fa-note-sticky',
name: 'muc-note-create-for-occupant'
})
buttons.push({
i18n_text: i18nSearch,
handler: async (ev) => {
ev.preventDefault()
await api.livechat_notes.searchNotesAbout(messageModel.occupant)
},
button_class: '',
icon_class: 'fa fa-magnifying-glass',
name: 'muc-note-search-for-occupant'
})
return buttons
}
export function getOccupantActionButtons (occupant, buttons) {
const muc = occupant.collection?.chatroom
if (!muc?.notes) {
// We dont have access.
return buttons
}
// eslint-disable-next-line no-undef
const i18nCreate = __(LOC_moderator_note_create_for_participant)
// eslint-disable-next-line no-undef
const i18nSearch = __(LOC_moderator_note_search_for_participant)
buttons.push({
i18n_text: i18nCreate,
handler: async (ev) => {
ev.preventDefault()
await api.livechat_notes.openCreateNoteForm(occupant)
},
button_class: '',
icon_class: 'fa fa-note-sticky',
name: 'muc-note-create-for-occupant'
})
buttons.push({
i18n_text: i18nSearch,
handler: async (ev) => {
ev.preventDefault()
await api.livechat_notes.searchNotesAbout(occupant)
},
button_class: '',
icon_class: 'fa fa-magnifying-glass',
name: 'muc-note-search-for-occupant'
})
return buttons
}
function _initChatRoomNotes (mucModel) {
if (mucModel.noteManager) {
// already initiliazed
return
}
mucModel.notes = new _converse.exports.ChatRoomNotes(undefined, { chatroom: mucModel })
mucModel.noteManager = new NotePubSubManager(
mucModel.get('jid'),
'livechat-notes', // the node name
{
note: {
itemTag: 'note',
xmlns: XMLNS_NOTE,
collection: mucModel.notes,
fields: {
description: String
},
attributes: {
order: Number
}
}
}
)
mucModel.noteManager.start().catch(err => console.log(err))
// We must requestUpdate for all message actions, to add the "create note" button.
// FIXME: this should not be done here (but it is simplier for now)
document.querySelectorAll('converse-message-actions').forEach(el => el.requestUpdate())
}
function _destroyChatRoomNotes (mucModel) {
if (!mucModel.noteManager) { return }
mucModel.noteManager.stop().catch(err => console.log(err))
mucModel.noteManager = undefined
mucModel.notes = undefined
// We must requestUpdate for all message actions, to remove the "create note" button.
// FIXME: this should not be done here (but it is simplier for now)
document.querySelectorAll('converse-message-actions').forEach(el => el.requestUpdate())
}
export function initOrDestroyChatRoomNotes (mucModel) {
if (mucModel.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC.
return _destroyChatRoomNotes(mucModel)
}
if (!api.settings.get('livechat_note_app_enabled')) {
// Feature disabled, no need to handle notes.
return _destroyChatRoomNotes(mucModel)
}
if (mucModel.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
return _destroyChatRoomNotes(mucModel)
}
// We must check disco features
// (if the chat is remote, the server could use a livechat version that does not support this feature)
if (!mucModel.features?.get?.(XMLNS_NOTE)) {
return _destroyChatRoomNotes(mucModel)
}
const myself = mucModel.getOwnOccupant()
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
// User must be admin or owner
return _destroyChatRoomNotes(mucModel)
}
return _initChatRoomNotes(mucModel)
}

View File

@ -4,7 +4,7 @@
import { XMLNS_POLL } from '../constants.js'
import { tplPollForm } from '../templates/poll-form.js'
import { CustomElement } from 'shared/components/element.js'
import { converse, api } from '@converse/headless/core'
import { converse, api, parsers } from '@converse/headless'
import { webForm2xForm } from '@converse/headless/utils/form'
import { __ } from 'i18n'
import '../styles/poll-form.scss'
@ -18,7 +18,6 @@ export default class MUCPollFormView extends CustomElement {
return {
model: { type: Object, attribute: true },
modal: { type: Object, attribute: true },
form_fields: { type: Object, attribute: false },
alert_message: { type: Object, attribute: false },
title: { type: String, attribute: false },
instructions: { type: String, attribute: false }
@ -27,6 +26,8 @@ export default class MUCPollFormView extends CustomElement {
_fieldTranslationMap = new Map()
xform = undefined
async initialize () {
this.alert_message = undefined
if (!this.model) {
@ -36,20 +37,18 @@ export default class MUCPollFormView extends CustomElement {
try {
this._initFieldTranslations()
const stanza = await this._fetchPollForm()
const query = stanza.querySelector('query')
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0]
const xform = parsers.parseXForm(stanza)
if (!xform) {
throw Error('Missing xform in stanza')
}
xform.fields?.map(f => this._translateField(f))
this.xform = xform
// eslint-disable-next-line no-undef
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
// eslint-disable-next-line no-undef
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
this.form_fields = Array.from(xform.querySelectorAll('field')).map(field => {
this._translateField(field)
return u.xForm2TemplateResult(field, stanza)
})
} catch (err) {
console.error(err)
this.alert_message = __('Error')
@ -86,10 +85,10 @@ export default class MUCPollFormView extends CustomElement {
}
_translateField (field) {
const v = field.getAttribute('var')
const v = field.var
const label = this._fieldTranslationMap.get(v)
if (label) {
field.setAttribute('label', label)
field.label = label
}
}
@ -114,7 +113,7 @@ export default class MUCPollFormView extends CustomElement {
await api.sendIQ(iq)
if (this.modal) {
this.modal.onHide()
this.modal.close()
}
} catch (err) {
if (u.isErrorStanza(err)) {

View File

@ -4,7 +4,7 @@
import { tplPoll } from '../templates/poll.js'
import { CustomElement } from 'shared/components/element.js'
import { converse, _converse, api } from '@converse/headless/core'
import { converse, _converse, api } from '@converse/headless'
import '../styles/poll.scss'
export default class MUCPollView extends CustomElement {

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse } from '../../../src/headless/core.js'
import { _converse, converse } from '../../../src/headless/index.js'
import { getHeadingButtons } from './utils.js'
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
import { __ } from 'i18n'

View File

@ -4,7 +4,7 @@
import { __ } from 'i18n'
import BaseModal from 'plugins/modal/modal.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
import { html } from 'lit'
@ -13,8 +13,8 @@ class PollFormModal extends BaseModal {
super.initialize()
}
onHide () {
super.onHide()
close () {
super.close()
api.modal.remove('livechat-converse-poll-form-modal')
}

View File

@ -5,6 +5,10 @@
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { html } from 'lit'
import { __ } from 'i18n'
import { converse } from '@converse/headless'
const u = converse.env.utils
export function tplPollForm (el) {
const i18nOk = __('Ok')
// eslint-disable-next-line no-undef
@ -13,10 +17,18 @@ export function tplPollForm (el) {
page: 'documentation/user/streamers/polls'
})
let formFieldTemplates
if (el.xform) {
const fields = el.xform.fields
formFieldTemplates = fields.map(field => {
return u.xFormField2TemplateResult(field)
})
}
return html`
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
${
el.form_fields
formFieldTemplates
? html`
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
<p class="title">
@ -30,9 +42,9 @@ export function tplPollForm (el) {
<p class="form-help instructions">${el.instructions}</p>
<div class="form-errors hidden"></div>
${el.form_fields}
${formFieldTemplates}
<fieldset class="buttons form-group">
<fieldset class="buttons">
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
</fieldset>
</form>`

View File

@ -63,7 +63,7 @@ function _tplChoice (el, currentPoll, choice, canVote) {
<div class="livechat-progress-bar">
<div
role="progressbar"
style="width: ${percent}%;"
style=${'width: ' + percent + '%;'}
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
></div>
<p>
@ -83,21 +83,21 @@ export function tplPoll (el, currentPoll, canVote) {
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
<p class="livechat-poll-question">
${currentPoll.over
? html`<button class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
? html`<button type="button" class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
<converse-icon class="fa fa-times" size="1em"></converse-icon>
</button>`
: ''
}
${el.collapsed
? html`
<button @click=${el.toggle} class="livechat-poll-toggle">
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa fa-angle-right"
size="1em"></converse-icon>
</button>`
: html`
<button @click=${el.toggle} class="livechat-poll-toggle">
<button type="button" @click=${el.toggle} class="livechat-poll-toggle">
<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa fa-angle-down"

View File

@ -3,12 +3,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { XMLNS_POLL } from './constants.js'
import { _converse, api } from '../../../src/headless/core.js'
import { _converse, api } from '../../../src/headless/index.js'
import { __ } from 'i18n'
export function getHeadingButtons (view, buttons) {
const muc = view.model
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC.
return buttons
}

View File

@ -2,7 +2,9 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse, api } from '../../../src/headless/core.js'
import { _converse, converse, api } from '../../../src/headless/index.js'
let currentSize
/**
* This plugin computes the available width of converse-root, and adds classes
@ -16,6 +18,27 @@ converse.plugins.add('livechat-converse-size', {
dependencies: [],
initialize () {
Object.assign(api, {
livechat_size: {
current: () => {
return currentSize
},
width_is: (sizes) => {
if (!Array.isArray(sizes)) {
sizes = [sizes]
}
if (!currentSize) { return false }
return sizes.includes(currentSize.width)
},
height_is: (sizes) => {
if (!Array.isArray(sizes)) {
sizes = [sizes]
}
if (!currentSize) { return false }
return sizes.includes(currentSize.height)
}
}
})
_converse.api.listen.on('connected', start)
_converse.api.listen.on('reconnected', start)
_converse.api.listen.on('disconnected', stop)
@ -42,6 +65,7 @@ function start () {
}
function stop () {
currentSize = undefined
rootResizeObserver.disconnect()
const root = document.querySelector('converse-root')
if (root) {
@ -60,8 +84,9 @@ function handle (el) {
el.setAttribute('livechat-converse-root-width', width)
el.setAttribute('livechat-converse-root-height', height)
api.trigger('livechatSizeChanged', {
currentSize = {
height: height,
width: width
})
}
api.trigger('livechatSizeChanged', Object.assign({}, currentSize)) // cloning...
}

View File

@ -2,36 +2,20 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { api } from '@converse/headless/core'
import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless'
import { MUCApp } from '../../../shared/components/muc-app/index.js'
import { tplMUCTaskApp } from '../templates/muc-task-app.js'
import '../styles/muc-task-app.scss'
/**
* Custom Element to display the Task Application.
*/
export default class MUCTaskApp extends CustomElement {
static get properties () {
return {
model: { type: Object, attribute: true }, // mucModel
show: { type: Boolean, attribute: false }
}
}
async initialize () {
this.show = api.settings.get('livechat_task_app_restore') &&
(window.sessionStorage?.getItem?.('livechat-converse-task-app-show') === '1')
}
export default class MUCTaskApp extends MUCApp {
restoreSettingName = 'livechat_task_app_restore'
sessionStorageRestoreKey = 'livechat-converse-task-app-show'
render () {
return tplMUCTaskApp(this, this.model)
}
toggleApp () {
this.show = !this.show
window.sessionStorage?.setItem?.('livechat-converse-task-app-show', this.show ? '1' : '')
}
}
api.elements.define('livechat-converse-muc-task-app', MUCTaskApp)

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import tplMucTaskList from '../templates/muc-task-list'
import { __ } from 'i18n'

View File

@ -2,17 +2,14 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import tplMucTaskLists from '../templates/muc-task-lists'
import { __ } from 'i18n'
import { DraggablesCustomElement } from '../../../shared/components/draggables/index.js'
import '../styles/muc-task-lists.scss'
import '../styles/muc-task-drag.scss'
export default class MUCTaskListsView extends CustomElement {
currentDraggedTask = null
export default class MUCTaskListsView extends DraggablesCustomElement {
static get properties () {
return {
model: { type: Object, attribute: true },
@ -27,42 +24,22 @@ export default class MUCTaskListsView extends CustomElement {
return
}
this.draggableTagName = 'livechat-converse-muc-task'
this.droppableTagNames = ['livechat-converse-muc-task', 'livechat-converse-muc-task-list']
this.droppableAlwaysBottomTagNames = ['livechat-converse-muc-task-list']
// Adding or removing a new task list: we must update.
this.listenTo(this.model, 'add', () => this.requestUpdate())
this.listenTo(this.model, 'remove', () => this.requestUpdate())
this.listenTo(this.model, 'sort', () => this.requestUpdate())
this._handleDragStartBinded = this._handleDragStart.bind(this)
this._handleDragOverBinded = this._handleDragOver.bind(this)
this._handleDragLeaveBinded = this._handleDragLeave.bind(this)
this._handleDragEndBinded = this._handleDragEnd.bind(this)
this._handleDropBinded = this._handleDrop.bind(this)
return super.initialize()
}
render () {
return tplMucTaskLists(this, this.model)
}
connectedCallback () {
super.connectedCallback()
this.currentDraggedTask = null
this.addEventListener('dragstart', this._handleDragStartBinded)
this.addEventListener('dragover', this._handleDragOverBinded)
this.addEventListener('dragleave', this._handleDragLeaveBinded)
this.addEventListener('dragend', this._handleDragEndBinded)
this.addEventListener('drop', this._handleDropBinded)
}
disconnectedCallback () {
super.disconnectedCallback()
this.currentDraggedTask = null
this.removeEventListener('dragstart', this._handleDragStartBinded)
this.removeEventListener('dragover', this._handleDragOverBinded)
this.removeEventListener('dragleave', this._handleDragLeaveBinded)
this.removeEventListener('dragend', this._handleDragEndBinded)
this.removeEventListener('drop', this._handleDropBinded)
}
async submitCreateTaskList (ev) {
ev.preventDefault()
@ -96,15 +73,7 @@ export default class MUCTaskListsView extends CustomElement {
}
}
_getParentTaskEl (target) {
return target.closest?.('livechat-converse-muc-task')
}
_getParentTaskOrTaskListEl (target) {
return target.closest?.('livechat-converse-muc-task, livechat-converse-muc-task-list')
}
_isATaskEl (target) {
isATaskEl (target) {
return target.nodeName?.toLowerCase() === 'livechat-converse-muc-task'
}
@ -112,71 +81,18 @@ export default class MUCTaskListsView extends CustomElement {
return target.nodeName?.toLowerCase() === 'livechat-converse-muc-task-list'
}
_isOnTopHalf (ev, taskEl) {
const y = ev.clientY
const bounding = taskEl.getBoundingClientRect()
return (y <= bounding.y + (bounding.height / 2))
}
_resetDropOver () {
document.querySelectorAll('.livechat-drag-bottom-half, .livechat-drag-top-half').forEach(
el => el.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
)
}
_handleDragStart (ev) {
// The draggable=true is on a livechat-converse-muc-task child
const possibleTaskEl = ev.target.parentElement
if (!this._isATaskEl(possibleTaskEl)) { return }
console.log('[livechat task drag&drop] Starting to drag a task...')
this.currentDraggedTask = possibleTaskEl
this._resetDropOver()
}
_handleDragOver (ev) {
if (!this.currentDraggedTask) { return }
const taskOrTaskListEl = this._getParentTaskOrTaskListEl(ev.target)
if (!taskOrTaskListEl) { return }
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event says we should preventDefault
ev.preventDefault()
// Are we on the top or bottom part of the taskEl?
// Note: for task list, we always add the task in the task list, so no need to test here.
const topHalf = this._isATaskEl(taskOrTaskListEl) ? this._isOnTopHalf(ev, taskOrTaskListEl) : false
taskOrTaskListEl.classList.add(topHalf ? 'livechat-drag-top-half' : 'livechat-drag-bottom-half')
taskOrTaskListEl.classList.remove(topHalf ? 'livechat-drag-bottom-half' : 'livechat-drag-top-half')
}
_handleDragLeave (ev) {
if (!this.currentDraggedTask) { return }
const taskOrTaskListEl = this._getParentTaskOrTaskListEl(ev.target)
if (!taskOrTaskListEl) { return }
taskOrTaskListEl.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
}
_handleDragEnd (_ev) {
this.currentDraggedTask = null
this._resetDropOver()
}
_handleDrop (_ev) {
if (!this.currentDraggedTask) { return }
const droppedOnEl = document.querySelector('.livechat-drag-bottom-half, .livechat-drag-top-half')
const droppedOntaskOrTaskListEl = this._getParentTaskOrTaskListEl(droppedOnEl)
if (!droppedOntaskOrTaskListEl) { return }
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
super._dropDone(...arguments)
console.log('[livechat task drag&drop] Task dropped...')
const task = this.currentDraggedTask.model
const task = draggedEl.model
let newOrder, targetTasklist
if (this.isATaskListEl(droppedOntaskOrTaskListEl)) {
if (this.isATaskListEl(droppedOnEl)) {
// We dropped on a task list, we must add as first entry.
newOrder = 0
targetTasklist = droppedOntaskOrTaskListEl.model
targetTasklist = droppedOnEl.model
if (task.get('list') !== targetTasklist.get('id')) {
console.log('[livechat task drag&drop] Changing task list...')
task.set('list', targetTasklist.get('id'))
@ -185,9 +101,9 @@ export default class MUCTaskListsView extends CustomElement {
console.log('[livechat task drag&drop] Task dropped on tasklist, but already first item, nothing to do')
return
}
} else if (this._isATaskEl(droppedOntaskOrTaskListEl)) {
} else if (this.isATaskEl(droppedOnEl)) {
// We dropped on a task, we must get its order (+1 if !onTopHalf)
const droppedOnTask = droppedOntaskOrTaskListEl.model
const droppedOnTask = droppedOnEl.model
if (task === droppedOnTask) {
// But of course, if dropped on itself there is nothing to do.
console.log('[livechat task drag&drop] Task dropped on itself, nothing to do')
@ -199,9 +115,8 @@ export default class MUCTaskListsView extends CustomElement {
task.set('list', droppedOnTask.get('list'))
}
const topHalf = droppedOnEl.classList.contains('livechat-drag-top-half')
newOrder = droppedOnTask.get('order') ?? 0
if (!topHalf) { newOrder = Math.max(0, newOrder + 1) }
if (!onTopHalf) { newOrder = Math.max(0, newOrder + 1) }
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
console.error(
@ -217,45 +132,7 @@ export default class MUCTaskListsView extends CustomElement {
return
}
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
console.error('[livechat task drag&drop] Computed new order is not a number, aborting.')
return
}
console.log('[livechat task drag&drop] Task new order will be ' + newOrder)
console.log('[livechat task drag&drop] Reordering tasks...')
let currentOrder = newOrder + 1
for (const t of targetTasklist.getTasks()) {
if (t === task) {
console.log('[livechat task drag&drop] Skipping the currently moved task')
continue
}
let order = t.get('order') ?? 0
if (typeof order !== 'number' || isNaN(order)) {
console.error('[livechat task drag&drop] Found a task with an invalid order, fixing it.')
order = currentOrder // this will cause the code bellow to increment task order
}
if (order < newOrder) { continue }
currentOrder++
if (order > currentOrder) {
console.log(
`Task "${t.get('name')}" as already on order greater than ${currentOrder.toString()}, stoping.`
)
break
}
console.log(`Changing order of task "${t.get('name')}" to ${currentOrder}`)
t.set('order', currentOrder)
t.saveItem() // TODO: handle errors?
}
console.log('[livechat task drag&drop] Setting new order on the moved task')
task.set('order', newOrder)
task.saveItem() // TODO: handle errors?
this._resetDropOver()
this._saveOrders(targetTasklist.getTasks(), task, newOrder)
}
}

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import { tplMucTask } from '../templates/muc-task'
import { __ } from 'i18n'

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { _converse, converse } from '../../../src/headless/core.js'
import { _converse, converse } from '../../../src/headless/index.js'
import { ChatRoomTaskLists } from './task-lists.js'
import { ChatRoomTaskList } from './task-list.js'
import { ChatRoomTasks } from './tasks.js'
@ -18,9 +18,14 @@ converse.plugins.add('livechat-converse-tasks', {
dependencies: ['converse-muc', 'converse-disco', 'converse-pubsub'],
initialize () {
_converse.ChatRoomTaskLists = ChatRoomTaskLists
_converse.ChatRoomTaskList = ChatRoomTaskList
_converse.ChatRoomTasks = ChatRoomTasks
Object.assign(
_converse.exports,
{
ChatRoomTaskLists,
ChatRoomTaskList,
ChatRoomTasks
}
)
_converse.api.settings.extend({
livechat_task_app_enabled: false,

View File

@ -4,7 +4,7 @@
import BaseModal from 'plugins/modal/modal.js'
import tplPickTaskList from './templates/pick-task-list.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import { __ } from 'i18n'
export default class PickTaskListModal extends BaseModal {

View File

@ -19,22 +19,22 @@ export default function (el) {
return html`
<form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onPick(ev)}>
<div class="form-group">
<select class="form-control" name="tasklist">
${
repeat(muc.tasklists, (tasklist) => tasklist.get('id'), (tasklist) => {
return html`<option value="${tasklist.get('id')}">${tasklist.get('name')}</option>`
})
}
</select>
<small class="form-text text-muted">
${i18nMessage}
</small>
</div>
<fieldset>
<select class="form-control" name="tasklist">
${
repeat(muc.tasklists, (tasklist) => tasklist.get('id'), (tasklist) => {
return html`<option value="${tasklist.get('id')}">${tasklist.get('name')}</option>`
})
}
</select>
<small class="form-text text-muted">
${i18nMessage}
</small>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-primary">${__('OK')}</button>
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
</div>
<fieldset>
<button type="submit" class="btn btn-primary">${__('OK')}</button>
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
</fieldset>
</form>`
}

View File

@ -1,27 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
livechat-converse-muc-task {
&.livechat-drag-bottom-half .task-line {
border-bottom: 4px solid blue;
}
&.livechat-drag-top-half .task-line {
border-top: 4px solid blue;
}
}
livechat-converse-muc-task-list {
&.livechat-drag-bottom-half .task-list-line {
border-bottom: 4px solid blue;
}
&.livechat-drag-top-half .task-list-line {
border-top: 4px solid blue;
}
}
}

View File

@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
/**
* A chat room task list.
* @class
* @namespace _converse.ChatRoomTaskList
* @namespace _converse.exports.ChatRoomTaskList
* @memberof _converse
*/
class ChatRoomTaskList extends Model {
@ -40,7 +40,7 @@ class ChatRoomTaskList extends Model {
data.list = this.get('id')
if (!data.order) {
data.order = 0 + Math.max(
data.order = 1 + Math.max(
0,
...(this.getTasks().map(t => t.get('order') ?? 0).filter(o => !isNaN(o)))
)

View File

@ -7,9 +7,9 @@ import { ChatRoomTaskList } from './task-list'
import { initStorage } from '@converse/headless/utils/storage.js'
/**
* A list of {@link _converse.ChatRoomTaskList} instances, representing task lists associated to a MUC.
* A list of {@link _converse.exports.ChatRoomTaskList} instances, representing task lists associated to a MUC.
* @class
* @namespace _converse.ChatRoomTaskLists
* @namespace _converse.exports.ChatRoomTaskLists
* @memberOf _converse
*/
class ChatRoomTaskLists extends Collection {

View File

@ -7,7 +7,7 @@ import { Model } from '@converse/skeletor/src/model.js'
/**
* A chat room task.
* @class
* @namespace _converse.ChatRoomTask
* @namespace _converse.exports.ChatRoomTask
* @memberof _converse
*/
class ChatRoomTask extends Model {

View File

@ -7,9 +7,9 @@ import { ChatRoomTask } from './task'
import { initStorage } from '@converse/headless/utils/storage.js'
/**
* A list of {@link _converse.ChatRoomTask} instances, representing all tasks associated to a MUC.
* A list of {@link _converse.exports.ChatRoomTask} instances, representing all tasks associated to a MUC.
* @class
* @namespace _converse.ChatRoomTasks
* @namespace _converse.exports.ChatRoomTasks
* @memberOf _converse
*/
class ChatRoomTasks extends Collection {

View File

@ -3,28 +3,24 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
import { tplMUCApp } from '../../../shared/components/muc-app/templates/muc-app.js'
import { html } from 'lit'
import { __ } from 'i18n'
export function tplMUCTaskApp (el, mucModel) {
if (!mucModel) {
// should not happen
el.classList.add('hidden') // we must do this, otherwise will have CSS side effects
return html``
}
if (!mucModel.tasklists) {
// too soon, not initialized yet (this will happen)
el.classList.add('hidden') // we must do this, otherwise will have CSS side effects
return html``
}
if (!el.show) {
el.classList.add('hidden')
return html``
}
el.classList.remove('hidden')
// eslint-disable-next-line no-undef
const i18nTasks = __(LOC_tasks)
// eslint-disable-next-line no-undef
@ -33,19 +29,11 @@ export function tplMUCTaskApp (el, mucModel) {
page: 'documentation/user/streamers/tasks'
})
return html`
<div class="livechat-converse-muc-app-header">
<h5>${i18nTasks}</h5>
<a href="${helpUrl}" target="_blank"><converse-icon
class="fa fa-circle-question"
size="1em"
title="${i18nHelp}"
></converse-icon></a>
<button class="livechat-converse-muc-app-close" @click=${el.toggleApp} title="${__('Close')}">
<converse-icon class="fa fa-times" size="1em"></converse-icon>
</button>
</div>
<div class="livechat-converse-muc-app-body">
<livechat-converse-muc-task-lists .model=${mucModel.tasklists}></livechat-converse-muc-task-lists>
</div>`
return tplMUCApp(
el,
i18nTasks,
helpUrl,
i18nHelp,
html`<livechat-converse-muc-task-lists .model=${mucModel.tasklists}></livechat-converse-muc-task-lists>`
)
}

View File

@ -16,17 +16,17 @@ export default function tplMucTaskList (el, tasklist) {
// eslint-disable-next-line no-undef
const i18nTaskListName = __(LOC_task_list_name)
return html`
<div class="task-list-line">
<div class="task-list-line draggables-line">
${el.collapsed
? html`
<button @click=${el.toggleTasks} class="task-list-toggle-tasks">
<button type="button" @click=${el.toggleTasks} class="task-list-toggle-tasks">
<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa fa-angle-right"
size="1em"></converse-icon>
</button>`
: html`
<button @click=${el.toggleTasks} class="task-list-toggle-tasks">
<button type="button" @click=${el.toggleTasks} class="task-list-toggle-tasks">
<converse-icon
color="var(--muc-toolbar-btn-color)"
class="fa fa-angle-down"
@ -38,15 +38,15 @@ export default function tplMucTaskList (el, tasklist) {
<div class="task-list-name">
<a @click=${el.toggleTasks}>${tasklist.get('name')}</a>
</div>
<button class="task-list-action" title="${i18nCreateTask}" @click=${el.openAddTaskForm}>
<button type="button" class="task-list-action" title="${i18nCreateTask}" @click=${el.openAddTaskForm}>
<converse-icon class="fa fa-plus" size="1em"></converse-icon>
</button>
<button class="task-list-action" title="${__('Edit')}"
<button type="button" class="task-list-action" title="${__('Edit')}"
@click=${el.toggleEdit}
>
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
</button>
<button class="task-list-action" title="${i18nDelete}"
<button type="button" class="task-list-action" title="${i18nDelete}"
@click=${el.deleteTaskList}
>
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>

View File

@ -24,7 +24,7 @@ export default function tplMucTaskLists (el, tasklists) {
})
}
<form class="converse-form" @submit=${el.submitCreateTaskList}>
<div class="form-group">
<fieldset>
<label>
${i18nCreateTaskList}
<input type="text" value="" class="form-control" name="name" placeholder="${i18nTaskListName}" />
@ -34,6 +34,6 @@ export default function tplMucTaskLists (el, tasklists) {
? ''
: html`<div class="invalid-feedback d-block">${el.create_tasklist_error_message}</div>`
}
</div>
</fieldset>
</form>`
}

View File

@ -13,7 +13,7 @@ export function tplMucTask (el, task) {
const doneId = 'livechat-task-done-id-' + task.get('id')
return !el.edit
? html`
<div draggable="true" class="task-line" ?task-is-done=${done}>
<div draggable="true" class="task-line draggables-line" ?task-is-done=${done}>
<div class="form-check">
<input
id="${doneId}"
@ -30,22 +30,22 @@ export function tplMucTask (el, task) {
</label>
</div>
<div class="task-description">${task.get('description') ?? ''}</div>
<button class="task-action" title="${__('Edit')}"
<button type="button" class="task-action" title="${__('Edit')}"
@click=${el.toggleEdit}
>
<converse-icon class="fa fa-edit" size="1em"></converse-icon>
</button>
<button class="task-action" title="${i18nDelete}"
<button type="button" class="task-action" title="${i18nDelete}"
@click=${el.deleteTask}
>
<converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
</button>
</div>`
: html`
<div class="task-line">
<div class="task-line draggables-line">
<form class="converse-form" @submit=${el.saveTask}>
${_tplTaskForm(task)}
<fieldset class="form-group">
<fieldset>
<input type="submit" class="btn btn-primary" value="${__('Ok')}" />
<input type="button" class="btn btn-secondary button-cancel"
value="${__('Cancel')}" @click=${el.toggleEdit}
@ -61,7 +61,7 @@ function _tplTaskForm (task) {
// eslint-disable-next-line no-undef
const i18nTaskDesc = __(LOC_task_description)
return html`<fieldset class="form-group">
return html`<fieldset>
<input type="text" name="name"
class="form-control" value="${task ? task.get('name') : ''}"
placeholder="${i18nTaskName}"
@ -80,7 +80,7 @@ export function tplMucAddTaskForm (tasklistEl, _tasklist) {
return html`
<form class="task-list-add-task converse-form" @submit=${tasklistEl.submitAddTask}>
${_tplTaskForm(undefined)}
<fieldset class="form-group">
<fieldset>
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
<input type="button" class="btn btn-secondary button-cancel"
value="${i18nCancel}" @click=${tasklistEl.closeAddTaskForm}

View File

@ -4,12 +4,12 @@
import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js'
import { PubSubManager } from '../../shared/lib/pubsub-manager.js'
import { converse, _converse, api } from '../../../src/headless/core.js'
import { converse, _converse, api } from '../../../src/headless/index.js'
import { __ } from 'i18n'
export function getHeadingButtons (view, buttons) {
const muc = view.model
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
if (muc.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC.
return buttons
}
@ -74,8 +74,8 @@ function _initChatRoomTaskLists (mucModel) {
return
}
mucModel.tasklists = new _converse.ChatRoomTaskLists(undefined, { chatroom: mucModel })
mucModel.tasks = new _converse.ChatRoomTasks(undefined, { chatroom: mucModel })
mucModel.tasklists = new _converse.exports.ChatRoomTaskLists(undefined, { chatroom: mucModel })
mucModel.tasks = new _converse.exports.ChatRoomTasks(undefined, { chatroom: mucModel })
mucModel.taskManager = new PubSubManager(
mucModel.get('jid'),
@ -127,7 +127,7 @@ function _destroyChatRoomTaskLists (mucModel) {
}
export function initOrDestroyChatRoomTaskLists (mucModel) {
if (mucModel.get('type') !== _converse.CHATROOMS_TYPE) {
if (mucModel.get('type') !== _converse.constants.CHATROOMS_TYPE) {
// only on MUC.
return _destroyChatRoomTaskLists(mucModel)
}

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import { html } from 'lit'
import { __ } from 'i18n'

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { converse, api } from '../../../src/headless/core.js'
import { converse, api } from '../../../src/headless/index.js'
import './components/muc-terms.js'
const { sizzle } = converse.env

View File

@ -0,0 +1,193 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import './styles/draggables.scss'
/**
* This is the base class for custom elements that contains draggable items.
*/
export class DraggablesCustomElement extends CustomElement {
currentDragged = null
/**
* The tag name for draggable elements.
* Example: livechat-converse-muc-note.
* Must be set in derived class.
*/
draggableTagName = 'invalid-tag-name'
/**
* The tag names on which we can drop the element.
* Examples: livechat-converse-muc-note, livechat-converse-muc-task, livechat-converse-muc-task-list.
* Must be set in derived class.
*/
droppableTagNames = []
/**
* Tag names for which we will always drop to bottom (for example: task lists)
*/
droppableAlwaysBottomTagNames = []
initialize () {
this._handleDragStartBinded = this._handleDragStart.bind(this)
this._handleDragOverBinded = this._handleDragOver.bind(this)
this._handleDragLeaveBinded = this._handleDragLeave.bind(this)
this._handleDragEndBinded = this._handleDragEnd.bind(this)
this._handleDropBinded = this._handleDrop.bind(this)
return super.initialize()
}
connectedCallback () {
super.connectedCallback()
this.currentDragged = null
this.addEventListener('dragstart', this._handleDragStartBinded)
this.addEventListener('dragover', this._handleDragOverBinded)
this.addEventListener('dragleave', this._handleDragLeaveBinded)
this.addEventListener('dragend', this._handleDragEndBinded)
this.addEventListener('drop', this._handleDropBinded)
}
disconnectedCallback () {
super.disconnectedCallback()
this.currentDragged = null
this.removeEventListener('dragstart', this._handleDragStartBinded)
this.removeEventListener('dragover', this._handleDragOverBinded)
this.removeEventListener('dragleave', this._handleDragLeaveBinded)
this.removeEventListener('dragend', this._handleDragEndBinded)
this.removeEventListener('drop', this._handleDropBinded)
}
_isADraggableEl (target) {
return target.nodeName?.toLowerCase() === this.draggableTagName
}
_getParentDroppableEl (target) {
return target.closest?.(this.droppableTagNames.join(','))
}
_isOnTopHalf (ev, el) {
const y = ev.clientY
const bounding = el.getBoundingClientRect()
return (y <= bounding.y + (bounding.height / 2))
}
_resetDropOver () {
document.querySelectorAll('.livechat-drag-bottom-half, .livechat-drag-top-half').forEach(
el => el.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
)
}
_handleDragStart (ev) {
// The draggable=true is on a child bode
const possibleEl = ev.target.parentElement
if (!this._isADraggableEl(possibleEl)) { return }
console.log('[livechat drag&drop] Starting to drag a ' + this.draggableTagName + '...')
this.currentDragged = possibleEl
this._resetDropOver()
}
_handleDragOver (ev) {
if (!this.currentDragged) { return }
const droppableEl = this._getParentDroppableEl(ev.target)
if (!droppableEl) { return }
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event says we should preventDefault
ev.preventDefault()
// Are we on the top or bottom part of the droppableEl?
let topHalf = false
if (!this.droppableAlwaysBottomTagNames.includes(droppableEl.nodeName.toLowerCase())) {
topHalf = this._isOnTopHalf(ev, droppableEl)
}
droppableEl.classList.add(topHalf ? 'livechat-drag-top-half' : 'livechat-drag-bottom-half')
droppableEl.classList.remove(topHalf ? 'livechat-drag-bottom-half' : 'livechat-drag-top-half')
}
_handleDragLeave (ev) {
if (!this.currentDragged) { return }
const el = this._getParentDroppableEl(ev.target)
if (!el) { return }
el.classList.remove('livechat-drag-bottom-half', 'livechat-drag-top-half')
}
_handleDragEnd (_ev) {
this.currentDragged = null
this._resetDropOver()
}
_handleDrop (_ev) {
if (!this.currentDragged) { return }
let droppedOnEl = document.querySelector('.livechat-drag-bottom-half, .livechat-drag-top-half')
droppedOnEl = this._getParentDroppableEl(droppedOnEl)
if (!droppedOnEl) { return }
console.log('[livechat drag&drop] ' + this.draggableTagName + ' dropped...')
try {
this._dropDone(this.currentDragged, droppedOnEl, droppedOnEl.classList.contains('livechat-drag-top-half'))
} catch (err) {
console.error(err)
}
this._resetDropOver()
}
/**
* The callback when a valid drop occurs.
* Must be overloaded.
*/
_dropDone (draggedEl, droppedOnEl, onTopHalf) {
console.debug('[livechat drag&drop] Drop done:', draggedEl, droppedOnEl, onTopHalf)
}
/**
* This method can be called from _dropDone to save the new objects orders.
* For it to work, models must respect following constraints:
* * be a Model
* * have the order attribute
* * have an id attribute (for logging)
* * have get, set and saveItem methods
*/
_saveOrders (models, currentModel, newOrder) {
if (typeof newOrder !== 'number' || isNaN(newOrder)) {
console.error('[livechat drag&drop] Computed new order is not a number, aborting.')
return
}
console.log('[livechat drag&drop] Reordering models... Model new order will be ' + newOrder)
let currentOrder = newOrder + 1
for (const m of models) {
if (m === currentModel) {
console.log('[livechat drag&drop] Skipping the currently moved model')
continue
}
let order = m.get('order') ?? 0
if (typeof order !== 'number' || isNaN(order)) {
console.error('[livechat drag&drop] Found a model with an invalid order, fixing it.')
order = currentOrder // this will cause the code bellow to increment model order
}
if (order < newOrder) { continue }
currentOrder++
if (order > currentOrder) {
console.log(
`Object "${m.get('id')}" as already on order greater than ${currentOrder.toString()}, stoping.`
)
break
}
console.log(`Changing order of model "${m.get('id')}" to ${currentOrder}`)
m.set('order', currentOrder)
m.saveItem() // TODO: handle errors?
}
console.log('[livechat drag&drop] Setting new order on the moved model')
currentModel.set('order', newOrder)
currentModel.saveItem() // TODO: handle errors?
}
}

View File

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.conversejs {
// FIXME: the use of ">" only works if the draggables-lines is a direct
// child of the element.
// We should find a better way to do this (and that will not break for nested
// elements, like task in tast-list).
.livechat-drag-bottom-half > .draggables-line {
border-bottom: 4px solid blue;
}
.livechat-drag-top-half > .draggables-line {
border-top: 4px solid blue;
}
}

View File

@ -4,7 +4,7 @@
/* eslint-disable max-len */
import { html } from 'lit'
import tplIcons from '../../../src/shared/templates/icons.js'
import tplIcons from '../../../src/shared/components/templates/icons.js'
export default () => {
// Here we are adding some additonal icons to ConverseJS defaults
@ -28,6 +28,16 @@ export default () => {
<symbol id="icon-square-poll-horizontal" viewBox="0 0 448 512">
<path d="M448 96c0-35.3-28.7-64-64-64L64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320zM256 160c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0c17.7 0 32 14.3 32 32zm64 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l-192 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l192 0zM192 352c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0c17.7 0 32 14.3 32 32z"/>
</symbol>
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<symbol id="icon-note-sticky" viewBox="0 0 448 512">
<path d="M64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l224 0 0-80c0-17.7 14.3-32 32-32l80 0 0-224c0-8.8-7.2-16-16-16L64 80zM288 480L64 480c-35.3 0-64-28.7-64-64L0 96C0 60.7 28.7 32 64 32l320 0c35.3 0 64 28.7 64 64l0 224 0 5.5c0 17-6.7 33.3-18.7 45.3l-90.5 90.5c-12 12-28.3 18.7-45.3 18.7l-5.5 0z"/>
</symbol>
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<symbol id="icon-magnifying-glass" viewBox="0 0 512 512">
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/>
</symbol>
</svg>
`
}

View File

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { CustomElement } from 'shared/components/element.js'
import { api, _converse } from '@converse/headless'
import './styles/muc-app.scss'
/**
* Base class for MUC App custom elements (task app, notes app, ...).
* This is an abstract class, should not be called directly.
*/
export class MUCApp extends CustomElement {
restoreSettingName = undefined // must be overloaded
sessionStorageRestoreKey = undefined // must be overloaded
static get properties () {
return {
model: { type: Object, attribute: true }, // mucModel
show: { type: Boolean, attribute: false }
}
}
async initialize () {
this.classList.add('livechat-converse-muc-app')
this.show = this.restoreSettingName &&
api.settings.get(this.restoreSettingName) &&
this.sessionStorageRestoreKey &&
(window.sessionStorage?.getItem?.(this.sessionStorageRestoreKey) === '1')
// we listen for livechatSizeChanged event,
// and close all apps except the first if small or medium width.
// Note: this will also be triggered when we first open the page
this.listenTo(_converse, 'livechatSizeChanged', () => {
if (!this.show || !api.livechat_size?.width_is(['small', 'medium'])) {
return
}
// are we the first opened app?
for (const el of document.querySelectorAll('.livechat-converse-muc-app')) {
if (el === this) { break }
if (!el.show) { continue }
console.debug('The livechat size is small or medium, there is already an opened app, so closing myself', this)
// ok, there is already an opened app.
this.toggleApp() // we know we are open
break
}
})
}
render () { // must be overloaded.
return ''
}
updated () {
if (this.innerText.trim() === '') {
this.classList.add('hidden') // we must do this, otherwise will have CSS side effects
} else {
this.classList.remove('hidden')
}
super.updated()
}
toggleApp () {
this.show = !this.show
if (this.sessionStorageRestoreKey) {
window.sessionStorage?.setItem?.(this.sessionStorageRestoreKey, this.show ? '1' : '')
}
if (
this.show &&
api.livechat_size?.width_is(['small', 'medium'])
) {
// When showing an App, if the screen width is small or medium, we hide the others.
this._closeOtherApps()
}
}
showApp () {
if (!this.show) { return this.toggleApp() }
}
hideApp () {
if (this.show) { return this.toggleApp() }
}
_closeOtherApps () {
document.querySelectorAll('.livechat-converse-muc-app').forEach((el) => {
if (el !== this && el.show) {
console.debug('Closing another app, because livechat width is small or medium', el)
el.toggleApp()
}
})
}
}

View File

@ -5,7 +5,7 @@
*/
.conversejs {
livechat-converse-muc-task-app {
.livechat-converse-muc-app {
border: var(--occupants-border-left);
display: flex;
flex-flow: column nowrap;
@ -42,8 +42,8 @@
&[livechat-converse-root-width="small"],
&[livechat-converse-root-width="medium"] {
converse-muc-chatarea livechat-converse-muc-task-app:not(.hidden) ~ * {
// on small and medium width, we hide all subsequent siblings of the task app
converse-muc-chatarea .livechat-converse-muc-app:not(.hidden) ~ * {
// on small and medium width, we hide all subsequent siblings of the app
// (when app is not hidden)
display: none !important;
}

View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
import { __ } from 'i18n'
export function tplMUCApp (el, i18nTitle, helpUrl, i18nHelp, content) {
return html`
<div class="livechat-converse-muc-app-header">
<h5>${i18nTitle}</h5>
<a href="${helpUrl}" target="_blank"><converse-icon
class="fa fa-circle-question"
size="1em"
title="${i18nHelp}"
></converse-icon></a>
<button type="button" class="livechat-converse-muc-app-close" @click=${el.toggleApp} title="${__('Close')}">
<converse-icon class="fa fa-times" size="1em"></converse-icon>
</button>
</div>
<div class="livechat-converse-muc-app-body">
${content}
</div>`
}

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import { converse, _converse, api } from '../../../src/headless/core.js'
import { converse, _converse, api } from '../../../src/headless/index.js'
const { $build, Strophe, $iq, sizzle } = converse.env
/**
@ -50,7 +50,7 @@ export class PubSubManager {
async start () {
// FIXME: handle errors. Find a way to display to user that this failed.
this.stanzaHandler = _converse.connection.addHandler(
this.stanzaHandler = api.connection.get().addHandler(
(message) => {
try {
this._handleMessage(message)
@ -79,7 +79,7 @@ export class PubSubManager {
// Note: no need to unsubscribe from the pubsub node, the backend will do when users leave the room.
if (this.stanzaHandler) {
_converse.connection.deleteHandler(this.stanzaHandler)
api.connection.get().deleteHandler(this.stanzaHandler)
this.stanzaHandler = undefined
}
}
@ -123,6 +123,7 @@ export class PubSubManager {
if (v === undefined) { continue }
data[field] = v
}
this._additionalModelToData(item, data)
console.log('Saving item...')
await this._save(type, data, id)
@ -178,6 +179,8 @@ export class PubSubManager {
item.c(fieldName).t(data[fieldName]).up()
}
this._additionalDataToItemNode(data, item)
await api.pubsub.publish(this.roomJID, this.node, item)
}
@ -336,6 +339,7 @@ export class PubSubManager {
}
}
}
this._additionalParseItemNode(itemNode, type, data)
return data
}
@ -351,4 +355,19 @@ export class PubSubManager {
_typeFromCollection (collection) {
return Object.values(this.types).find(type => type.collection === collection)
}
/**
* Overload to add some custom code for model to data conversion.
*/
_additionalModelToData (_item, _data) {}
/**
* Overload to add some custom code for data to stanza conversion.
*/
_additionalDataToItemNode (_data, _item) {}
/**
* Overload to add some custom code item parsing.
*/
_additionalParseItemNode (_itemNode, _type, _data) {}
}

View File

@ -4,7 +4,7 @@
import { __ } from 'i18n'
import BaseModal from 'plugins/modal/modal.js'
import { api } from '@converse/headless/core'
import { api } from '@converse/headless'
import { html } from 'lit'
import 'livechat-external-login-content.js'
@ -20,8 +20,8 @@ class ExternalLoginModal extends BaseModal {
return __(LOC_login_using_external_account)
}
onHide () {
super.onHide()
close () {
super.close()
// kill the externalAuthGetResult handler if still there
try {
if (window.externalAuthGetResult) { window.externalAuthGetResult() }

View File

@ -8,7 +8,7 @@
.dropdown-menu {
// Fixing all dropdown colors
--text-color: #212529; // default bootstrap color for dropdown-items
--text-color-lighten-15-percent: #8c8c8c; // default ConverseJS theme color
--inverse-link-color: #8c8c8c; // default ConverseJS theme color
background-color: #fff; // this is the default bootstrap color, used by ConverseJS
@ -27,6 +27,7 @@
border: 1px dashed var(--peertube-menu-background);
color: var(--peertube-main-foreground);
background-color: var(--peertube-main-background);
margin: 0 5px;
.livechat-hide-slow-mode-info-box {
cursor: pointer;

View File

@ -34,12 +34,16 @@ body.converse-fullscreen.theme-peertube,
body.converse-embedded converse-root.theme-peertube {
--foreground: var(--peertube-main-foreground);
--background: var(--peertube-main-background);
--badge-color: var(--background);
--button-hover-text-color: var(--background);
--subdued-color: #a8aba1;
--muc-color: var(--peertube-button-background);
--green: #3aa569; // only in this file
--redder-orange: #e77051; // only in this file
--orange: #e7a151; // only in this file
--light-blue: #578ea9; // only in this file
--lighter-blue: #85b47b; // only in this file
--chat-color: var(--green); // FIXME: copied from Converse. Is there side effects?
--chat-status-online: var(--green);
--chat-status-busy: var(--redder-orange);
--chat-status-away: var(--orange);
@ -55,7 +59,6 @@ body.converse-embedded converse-root.theme-peertube {
--text-shadow-color: var(--peertube-main-background); // FIXME: should be a little different from background
--text-color: var(--peertube-input-foreground);
--controlbox-text-color: var(--peertube-input-foreground); // Note: controlbox is not used
--text-color-lighten-15-percent: var(--peertube-input-foreground);
--message-text-color: var(--peertube-input-foreground);
--message-receipt-color: var(--green);
--save-button-color: var(--green);
@ -73,7 +76,6 @@ body.converse-embedded converse-root.theme-peertube {
--chat-correcting-color: var(--peertube-grey-background);
--chat-head-color-dark: #1e9652; // should not be used in this plugin
--chat-head-color-darker: #0e763b; // should not be used in this plugin
--chat-head-color-lighten-50-percent: #e7f7ee; // should not be used in this plugin
--chat-head-color: var(--green);
--chat-head-text-color: var(--peertube-input-foreground);
--chat-toolbar-btn-color: var(--peertube-button-background);
@ -106,7 +108,6 @@ body.converse-embedded converse-root.theme-peertube {
--controlbox-pane-background-color: #333;
--controlbox-pane-bg-hover-color: #464646;
--panel-divider-color: #333;
--chat-gutter: 0.5em;
--minimized-chats-width: 130px;
--mobile-chat-width: 100%;
--mobile-chat-height: 400px;
@ -119,9 +120,10 @@ body.converse-embedded converse-root.theme-peertube {
--chatroom-badge-color: var(--peertube-button-background);
--chatroom-badge-hover-color: var(--peertube-button-background);
--chatroom-correcting-color: var(--peertube-grey-background);
--chatroom-head-bg-color-dark: #d24e2b;
--chatroom-head-bg-color-dark: var(--peertube-button-background);
--chatroom-head-bg-color: var(--peertube-menu-background);
--chatroom-head-border-bottom: 1px solid var(--peertube-grey-foreground);
--chatroom-head-border-bottom: 0.15em solid var(--peertube-grey-foreground);
--chatroom-head-fg-color: var(--subdued-color);
--chatroom-head-button-color: #999;
--chatroom-head-color: var(--peertube-menu-foreground);
--chatroom-head-description-border-left: 1px solid #ddd;
@ -163,6 +165,7 @@ body.converse-embedded converse-root.theme-peertube {
--fullpage-chat-width: 100%;
--fullpage-emoji-picker-height: 300px;
--fullpage-max-chat-textarea-height: 15em;
--overlayed-chat-gutter: 1em;
--overlayed-chat-head-height: 55px;
--overlayed-chat-height: 450px;
--overlayed-chat-width: 300px;

View File

@ -60,7 +60,7 @@ body.livechat-readonly.livechat-noscroll {
}
}
// Viewer mode
// Viewer mode (before the user has chosen its nickname)
.livechat-viewer-mode-content {
display: none;
@ -73,7 +73,7 @@ body.livechat-readonly.livechat-noscroll {
gap: 0.5em 10px;
align-items: baseline;
.form-group,
fieldset,
label {
margin-bottom: 0 !important; // replaced by the gap on .livechat-viewer-mode-content
}
@ -171,7 +171,8 @@ body.converse-embedded {
#peertube-plugin-livechat-container {
converse-muc-message-form {
// For an unknown reason, message field in truncated... so adding a bottom margin.
margin-bottom: 6px;
// We also add left and right margin, as Converse v11 adds a g-0 class on converse-muc-chatarea
margin: 0 1px 6px 5px;
}
}
@ -187,4 +188,44 @@ body.converse-embedded {
// So we must revert appearance:
appearance: revert !important;
}
.toolbar-buttons {
// Converse v11 removed the toggle_occupant button on the right.
// To add it back, we must ensure that this toolbar takes all the width, and
// that the toggle-occupants button is on the right.
flex-grow: 2;
.toggle-occupants {
// Cancelling the flex-grow from btn-group
flex-grow: 0 !important;
// This margin-left trick is to align the button on the right.
margin-left: auto !important;
order: 99;
}
}
}
/* stylelint-disable-next-line no-descending-specificity */
#conversejs { // here we use the id have gretter priority
// These CSS are tricks: Converse v11 tries to hide the MUC when screen width is under 768px.
// We don't want that, so we cancel the d-none.
// FIXME: these hacks should be temporary, waiting for some improvement on Converse.
converse-muc-chatarea {
.chat-area.d-none {
display: flex !important;
}
/* stylelint-disable-next-line no-descending-specificity */
converse-muc-sidebar {
// we must not use !important for flex, it would break resizing.
// That's why we use #conversejs insteand of .conversejs for this block.
flex: 0 0 min(400px, 50%);
min-width: min(200px, 50%) !important;
.occupants {
width: 100%;
}
}
}
}

View File

@ -5,7 +5,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { html } from 'lit'
import { api } from '@converse/headless/core.js'
import { api } from '@converse/headless/index.js'
export default () => html`
<div class="inner-content converse-brand row">

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