diff --git a/webroot/favicon.ico b/webroot/favicon.ico deleted file mode 100644 index c6e2f9f..0000000 Binary files a/webroot/favicon.ico and /dev/null differ diff --git a/webroot/img/airplay.png b/webroot/img/airplay.png deleted file mode 100644 index 6529f69..0000000 Binary files a/webroot/img/airplay.png and /dev/null differ diff --git a/webroot/img/ban-user-grey.svg b/webroot/img/ban-user-grey.svg deleted file mode 100644 index 6cdca98..0000000 --- a/webroot/img/ban-user-grey.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/ban-user.svg b/webroot/img/ban-user.svg deleted file mode 100644 index dd5079d..0000000 --- a/webroot/img/ban-user.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/emoji/Reaper-gg.png b/webroot/img/emoji/Reaper-gg.png deleted file mode 100644 index de6d571..0000000 Binary files a/webroot/img/emoji/Reaper-gg.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-hi.png b/webroot/img/emoji/Reaper-hi.png deleted file mode 100644 index dc6b6ae..0000000 Binary files a/webroot/img/emoji/Reaper-hi.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-hype.png b/webroot/img/emoji/Reaper-hype.png deleted file mode 100644 index 2138dba..0000000 Binary files a/webroot/img/emoji/Reaper-hype.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-lol.png b/webroot/img/emoji/Reaper-lol.png deleted file mode 100644 index 8c4eff2..0000000 Binary files a/webroot/img/emoji/Reaper-lol.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-love.png b/webroot/img/emoji/Reaper-love.png deleted file mode 100644 index 5e0f3ba..0000000 Binary files a/webroot/img/emoji/Reaper-love.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-rage.png b/webroot/img/emoji/Reaper-rage.png deleted file mode 100644 index 3da0a5d..0000000 Binary files a/webroot/img/emoji/Reaper-rage.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-rip.png b/webroot/img/emoji/Reaper-rip.png deleted file mode 100644 index 89c13c7..0000000 Binary files a/webroot/img/emoji/Reaper-rip.png and /dev/null differ diff --git a/webroot/img/emoji/Reaper-wtf.png b/webroot/img/emoji/Reaper-wtf.png deleted file mode 100644 index e21adb6..0000000 Binary files a/webroot/img/emoji/Reaper-wtf.png and /dev/null differ diff --git a/webroot/img/emoji/ac-box.png b/webroot/img/emoji/ac-box.png deleted file mode 100644 index 18bd9b3..0000000 Binary files a/webroot/img/emoji/ac-box.png and /dev/null differ diff --git a/webroot/img/emoji/ac-construction.png b/webroot/img/emoji/ac-construction.png deleted file mode 100644 index ee2b6f3..0000000 Binary files a/webroot/img/emoji/ac-construction.png and /dev/null differ diff --git a/webroot/img/emoji/ac-fossil.png b/webroot/img/emoji/ac-fossil.png deleted file mode 100644 index 16d4fad..0000000 Binary files a/webroot/img/emoji/ac-fossil.png and /dev/null differ diff --git a/webroot/img/emoji/ac-item-leaf.png b/webroot/img/emoji/ac-item-leaf.png deleted file mode 100644 index 6abede7..0000000 Binary files a/webroot/img/emoji/ac-item-leaf.png and /dev/null differ diff --git a/webroot/img/emoji/ac-kkslider.png b/webroot/img/emoji/ac-kkslider.png deleted file mode 100644 index 309d768..0000000 Binary files a/webroot/img/emoji/ac-kkslider.png and /dev/null differ diff --git a/webroot/img/emoji/ac-moneytree.png b/webroot/img/emoji/ac-moneytree.png deleted file mode 100644 index 3e370ac..0000000 Binary files a/webroot/img/emoji/ac-moneytree.png and /dev/null differ diff --git a/webroot/img/emoji/ac-mosquito.png b/webroot/img/emoji/ac-mosquito.png deleted file mode 100644 index 50ddb41..0000000 Binary files a/webroot/img/emoji/ac-mosquito.png and /dev/null differ diff --git a/webroot/img/emoji/ac-shirt.png b/webroot/img/emoji/ac-shirt.png deleted file mode 100644 index 41ab1d8..0000000 Binary files a/webroot/img/emoji/ac-shirt.png and /dev/null differ diff --git a/webroot/img/emoji/ac-song.png b/webroot/img/emoji/ac-song.png deleted file mode 100644 index dfe9d84..0000000 Binary files a/webroot/img/emoji/ac-song.png and /dev/null differ diff --git a/webroot/img/emoji/ac-tree.png b/webroot/img/emoji/ac-tree.png deleted file mode 100644 index 499ee4e..0000000 Binary files a/webroot/img/emoji/ac-tree.png and /dev/null differ diff --git a/webroot/img/emoji/ac-turnip.png b/webroot/img/emoji/ac-turnip.png deleted file mode 100644 index 893ef9c..0000000 Binary files a/webroot/img/emoji/ac-turnip.png and /dev/null differ diff --git a/webroot/img/emoji/ac-weeds.png b/webroot/img/emoji/ac-weeds.png deleted file mode 100644 index 6d3b237..0000000 Binary files a/webroot/img/emoji/ac-weeds.png and /dev/null differ diff --git a/webroot/img/emoji/alert.gif b/webroot/img/emoji/alert.gif deleted file mode 100644 index d99ac1b..0000000 Binary files a/webroot/img/emoji/alert.gif and /dev/null differ diff --git a/webroot/img/emoji/bananadance.gif b/webroot/img/emoji/bananadance.gif deleted file mode 100644 index da6f470..0000000 Binary files a/webroot/img/emoji/bananadance.gif and /dev/null differ diff --git a/webroot/img/emoji/bb8.png b/webroot/img/emoji/bb8.png deleted file mode 100644 index e97e3f3..0000000 Binary files a/webroot/img/emoji/bb8.png and /dev/null differ diff --git a/webroot/img/emoji/beerparrot.gif b/webroot/img/emoji/beerparrot.gif deleted file mode 100644 index 2cd83ee..0000000 Binary files a/webroot/img/emoji/beerparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/bells.png b/webroot/img/emoji/bells.png deleted file mode 100644 index 1a20ab0..0000000 Binary files a/webroot/img/emoji/bells.png and /dev/null differ diff --git a/webroot/img/emoji/birthdaypartyparrot.gif b/webroot/img/emoji/birthdaypartyparrot.gif deleted file mode 100644 index 4c166ea..0000000 Binary files a/webroot/img/emoji/birthdaypartyparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/blacklightsaber.png b/webroot/img/emoji/blacklightsaber.png deleted file mode 100644 index 1a53796..0000000 Binary files a/webroot/img/emoji/blacklightsaber.png and /dev/null differ diff --git a/webroot/img/emoji/bluelightsaber.png b/webroot/img/emoji/bluelightsaber.png deleted file mode 100644 index b509c73..0000000 Binary files a/webroot/img/emoji/bluelightsaber.png and /dev/null differ diff --git a/webroot/img/emoji/bluntparrot.gif b/webroot/img/emoji/bluntparrot.gif deleted file mode 100644 index 6d33f7d..0000000 Binary files a/webroot/img/emoji/bluntparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/bobaparrot.gif b/webroot/img/emoji/bobaparrot.gif deleted file mode 100644 index 17ecdfa..0000000 Binary files a/webroot/img/emoji/bobaparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/cakeparrot.gif b/webroot/img/emoji/cakeparrot.gif deleted file mode 100644 index 8ddb47f..0000000 Binary files a/webroot/img/emoji/cakeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/chewbacca.png b/webroot/img/emoji/chewbacca.png deleted file mode 100644 index 4cae67b..0000000 Binary files a/webroot/img/emoji/chewbacca.png and /dev/null differ diff --git a/webroot/img/emoji/chillparrot.gif b/webroot/img/emoji/chillparrot.gif deleted file mode 100644 index 0d7a560..0000000 Binary files a/webroot/img/emoji/chillparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/christmasparrot.gif b/webroot/img/emoji/christmasparrot.gif deleted file mode 100644 index 417c3e0..0000000 Binary files a/webroot/img/emoji/christmasparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/coffeeparrot.gif b/webroot/img/emoji/coffeeparrot.gif deleted file mode 100644 index 3a935d2..0000000 Binary files a/webroot/img/emoji/coffeeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/confusedparrot.gif b/webroot/img/emoji/confusedparrot.gif deleted file mode 100644 index e641f43..0000000 Binary files a/webroot/img/emoji/confusedparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/copparrot.gif b/webroot/img/emoji/copparrot.gif deleted file mode 100644 index b41a474..0000000 Binary files a/webroot/img/emoji/copparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/coronavirus.png b/webroot/img/emoji/coronavirus.png deleted file mode 100644 index 6c65664..0000000 Binary files a/webroot/img/emoji/coronavirus.png and /dev/null differ diff --git a/webroot/img/emoji/covid19parrot.gif b/webroot/img/emoji/covid19parrot.gif deleted file mode 100644 index 70beb62..0000000 Binary files a/webroot/img/emoji/covid19parrot.gif and /dev/null differ diff --git a/webroot/img/emoji/cryptoparrot.gif b/webroot/img/emoji/cryptoparrot.gif deleted file mode 100644 index 0d5a8c9..0000000 Binary files a/webroot/img/emoji/cryptoparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/dabparrot.gif b/webroot/img/emoji/dabparrot.gif deleted file mode 100644 index 5f95dd4..0000000 Binary files a/webroot/img/emoji/dabparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/dadparrot.gif b/webroot/img/emoji/dadparrot.gif deleted file mode 100644 index 5e025dd..0000000 Binary files a/webroot/img/emoji/dadparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/daftpunkparrot.gif b/webroot/img/emoji/daftpunkparrot.gif deleted file mode 100644 index be7a1cf..0000000 Binary files a/webroot/img/emoji/daftpunkparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/darkbeerparrot.gif b/webroot/img/emoji/darkbeerparrot.gif deleted file mode 100644 index c37edb5..0000000 Binary files a/webroot/img/emoji/darkbeerparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/darkmodeparrot.gif b/webroot/img/emoji/darkmodeparrot.gif deleted file mode 100644 index b9ed205..0000000 Binary files a/webroot/img/emoji/darkmodeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/darth_vader.png b/webroot/img/emoji/darth_vader.png deleted file mode 100644 index 44f43d0..0000000 Binary files a/webroot/img/emoji/darth_vader.png and /dev/null differ diff --git a/webroot/img/emoji/dealwithitparrot.gif b/webroot/img/emoji/dealwithitparrot.gif deleted file mode 100644 index f501e5e..0000000 Binary files a/webroot/img/emoji/dealwithitparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/death_star.png b/webroot/img/emoji/death_star.png deleted file mode 100644 index 003a28a..0000000 Binary files a/webroot/img/emoji/death_star.png and /dev/null differ diff --git a/webroot/img/emoji/discoparrot.gif b/webroot/img/emoji/discoparrot.gif deleted file mode 100644 index 47a7548..0000000 Binary files a/webroot/img/emoji/discoparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/division-gg.png b/webroot/img/emoji/division-gg.png deleted file mode 100644 index d704b46..0000000 Binary files a/webroot/img/emoji/division-gg.png and /dev/null differ diff --git a/webroot/img/emoji/division-hi.png b/webroot/img/emoji/division-hi.png deleted file mode 100644 index 437b368..0000000 Binary files a/webroot/img/emoji/division-hi.png and /dev/null differ diff --git a/webroot/img/emoji/division-hype.png b/webroot/img/emoji/division-hype.png deleted file mode 100644 index b6260bc..0000000 Binary files a/webroot/img/emoji/division-hype.png and /dev/null differ diff --git a/webroot/img/emoji/division-lol.png b/webroot/img/emoji/division-lol.png deleted file mode 100644 index d085493..0000000 Binary files a/webroot/img/emoji/division-lol.png and /dev/null differ diff --git a/webroot/img/emoji/division-omg.png b/webroot/img/emoji/division-omg.png deleted file mode 100644 index b1100cf..0000000 Binary files a/webroot/img/emoji/division-omg.png and /dev/null differ diff --git a/webroot/img/emoji/division-rage.png b/webroot/img/emoji/division-rage.png deleted file mode 100644 index 55d13a3..0000000 Binary files a/webroot/img/emoji/division-rage.png and /dev/null differ diff --git a/webroot/img/emoji/division-rip.png b/webroot/img/emoji/division-rip.png deleted file mode 100644 index f57c78b..0000000 Binary files a/webroot/img/emoji/division-rip.png and /dev/null differ diff --git a/webroot/img/emoji/division-wtf.png b/webroot/img/emoji/division-wtf.png deleted file mode 100644 index 654cde9..0000000 Binary files a/webroot/img/emoji/division-wtf.png and /dev/null differ diff --git a/webroot/img/emoji/docparrot.gif b/webroot/img/emoji/docparrot.gif deleted file mode 100644 index 2fb502d..0000000 Binary files a/webroot/img/emoji/docparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/donutparrot.gif b/webroot/img/emoji/donutparrot.gif deleted file mode 100644 index 9137e90..0000000 Binary files a/webroot/img/emoji/donutparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/doom_mad.gif b/webroot/img/emoji/doom_mad.gif deleted file mode 100644 index 1f124b8..0000000 Binary files a/webroot/img/emoji/doom_mad.gif and /dev/null differ diff --git a/webroot/img/emoji/empire.png b/webroot/img/emoji/empire.png deleted file mode 100644 index da2fb8c..0000000 Binary files a/webroot/img/emoji/empire.png and /dev/null differ diff --git a/webroot/img/emoji/everythingsfineparrot.gif b/webroot/img/emoji/everythingsfineparrot.gif deleted file mode 100644 index 5680c32..0000000 Binary files a/webroot/img/emoji/everythingsfineparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/evilparrot.gif b/webroot/img/emoji/evilparrot.gif deleted file mode 100644 index edb93c0..0000000 Binary files a/webroot/img/emoji/evilparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/explodyparrot.gif b/webroot/img/emoji/explodyparrot.gif deleted file mode 100644 index 387005d..0000000 Binary files a/webroot/img/emoji/explodyparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/fixparrot.gif b/webroot/img/emoji/fixparrot.gif deleted file mode 100644 index 3e91ba7..0000000 Binary files a/webroot/img/emoji/fixparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/flyingmoneyparrot.gif b/webroot/img/emoji/flyingmoneyparrot.gif deleted file mode 100644 index abf389d..0000000 Binary files a/webroot/img/emoji/flyingmoneyparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/footballparrot.gif b/webroot/img/emoji/footballparrot.gif deleted file mode 100644 index 49472b7..0000000 Binary files a/webroot/img/emoji/footballparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/gabe1.png b/webroot/img/emoji/gabe1.png deleted file mode 100644 index 89b8c4e..0000000 Binary files a/webroot/img/emoji/gabe1.png and /dev/null differ diff --git a/webroot/img/emoji/gabe2.png b/webroot/img/emoji/gabe2.png deleted file mode 100644 index 987d91c..0000000 Binary files a/webroot/img/emoji/gabe2.png and /dev/null differ diff --git a/webroot/img/emoji/gentlemanparrot.gif b/webroot/img/emoji/gentlemanparrot.gif deleted file mode 100644 index 8e8f05b..0000000 Binary files a/webroot/img/emoji/gentlemanparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/githubparrot.gif b/webroot/img/emoji/githubparrot.gif deleted file mode 100644 index 0fa6ce5..0000000 Binary files a/webroot/img/emoji/githubparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/goomba.gif b/webroot/img/emoji/goomba.gif deleted file mode 100644 index c6bb719..0000000 Binary files a/webroot/img/emoji/goomba.gif and /dev/null differ diff --git a/webroot/img/emoji/gothparrot.gif b/webroot/img/emoji/gothparrot.gif deleted file mode 100644 index 361b68e..0000000 Binary files a/webroot/img/emoji/gothparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/hamburgerparrot.gif b/webroot/img/emoji/hamburgerparrot.gif deleted file mode 100644 index 192b0ff..0000000 Binary files a/webroot/img/emoji/hamburgerparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/harrypotterparrot.gif b/webroot/img/emoji/harrypotterparrot.gif deleted file mode 100644 index 032a37e..0000000 Binary files a/webroot/img/emoji/harrypotterparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/headbangingparrot.gif b/webroot/img/emoji/headbangingparrot.gif deleted file mode 100644 index 9aad2ec..0000000 Binary files a/webroot/img/emoji/headbangingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/headingparrot.gif b/webroot/img/emoji/headingparrot.gif deleted file mode 100644 index b17002c..0000000 Binary files a/webroot/img/emoji/headingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/headsetparrot.gif b/webroot/img/emoji/headsetparrot.gif deleted file mode 100644 index 516a04f..0000000 Binary files a/webroot/img/emoji/headsetparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/hmmparrot.gif b/webroot/img/emoji/hmmparrot.gif deleted file mode 100644 index 223a6c2..0000000 Binary files a/webroot/img/emoji/hmmparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/hypnoparrot.gif b/webroot/img/emoji/hypnoparrot.gif deleted file mode 100644 index a6a0983..0000000 Binary files a/webroot/img/emoji/hypnoparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/icecreamparrot.gif b/webroot/img/emoji/icecreamparrot.gif deleted file mode 100644 index 0a5093f..0000000 Binary files a/webroot/img/emoji/icecreamparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/illuminatiparrot.gif b/webroot/img/emoji/illuminatiparrot.gif deleted file mode 100644 index a0c4e79..0000000 Binary files a/webroot/img/emoji/illuminatiparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/jediparrot.gif b/webroot/img/emoji/jediparrot.gif deleted file mode 100644 index 690b41d..0000000 Binary files a/webroot/img/emoji/jediparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/keanu_thanks.gif b/webroot/img/emoji/keanu_thanks.gif deleted file mode 100644 index 989b9b7..0000000 Binary files a/webroot/img/emoji/keanu_thanks.gif and /dev/null differ diff --git a/webroot/img/emoji/laptop_parrot.gif b/webroot/img/emoji/laptop_parrot.gif deleted file mode 100644 index b14bb18..0000000 Binary files a/webroot/img/emoji/laptop_parrot.gif and /dev/null differ diff --git a/webroot/img/emoji/loveparrot.gif b/webroot/img/emoji/loveparrot.gif deleted file mode 100644 index c0d14ed..0000000 Binary files a/webroot/img/emoji/loveparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/mandalorian.png b/webroot/img/emoji/mandalorian.png deleted file mode 100644 index 7db4a3b..0000000 Binary files a/webroot/img/emoji/mandalorian.png and /dev/null differ diff --git a/webroot/img/emoji/margaritaparrot.gif b/webroot/img/emoji/margaritaparrot.gif deleted file mode 100644 index 10a8c6a..0000000 Binary files a/webroot/img/emoji/margaritaparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/mario.gif b/webroot/img/emoji/mario.gif deleted file mode 100644 index 87f1133..0000000 Binary files a/webroot/img/emoji/mario.gif and /dev/null differ diff --git a/webroot/img/emoji/matrixparrot.gif b/webroot/img/emoji/matrixparrot.gif deleted file mode 100644 index eaf4404..0000000 Binary files a/webroot/img/emoji/matrixparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/meldparrot.gif b/webroot/img/emoji/meldparrot.gif deleted file mode 100644 index ae46053..0000000 Binary files a/webroot/img/emoji/meldparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/metalparrot.gif b/webroot/img/emoji/metalparrot.gif deleted file mode 100644 index f358b2f..0000000 Binary files a/webroot/img/emoji/metalparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/michaeljacksonparrot.gif b/webroot/img/emoji/michaeljacksonparrot.gif deleted file mode 100644 index cfa8405..0000000 Binary files a/webroot/img/emoji/michaeljacksonparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/moonparrot.gif b/webroot/img/emoji/moonparrot.gif deleted file mode 100644 index 098938a..0000000 Binary files a/webroot/img/emoji/moonparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/moonwalkingparrot.gif b/webroot/img/emoji/moonwalkingparrot.gif deleted file mode 100644 index 873f828..0000000 Binary files a/webroot/img/emoji/moonwalkingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/mustacheparrot.gif b/webroot/img/emoji/mustacheparrot.gif deleted file mode 100644 index e71fe28..0000000 Binary files a/webroot/img/emoji/mustacheparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/nicolas_cage_party.gif b/webroot/img/emoji/nicolas_cage_party.gif deleted file mode 100644 index 5414317..0000000 Binary files a/webroot/img/emoji/nicolas_cage_party.gif and /dev/null differ diff --git a/webroot/img/emoji/nodeparrot.gif b/webroot/img/emoji/nodeparrot.gif deleted file mode 100644 index 6735b0b..0000000 Binary files a/webroot/img/emoji/nodeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/norwegianblueparrot.gif b/webroot/img/emoji/norwegianblueparrot.gif deleted file mode 100644 index 0aa9583..0000000 Binary files a/webroot/img/emoji/norwegianblueparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/opensourceparrot.gif b/webroot/img/emoji/opensourceparrot.gif deleted file mode 100644 index 7067743..0000000 Binary files a/webroot/img/emoji/opensourceparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/originalparrot.gif b/webroot/img/emoji/originalparrot.gif deleted file mode 100644 index 428cc22..0000000 Binary files a/webroot/img/emoji/originalparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/owncast.png b/webroot/img/emoji/owncast.png deleted file mode 100644 index 6e7fdc9..0000000 Binary files a/webroot/img/emoji/owncast.png and /dev/null differ diff --git a/webroot/img/emoji/palpatine.png b/webroot/img/emoji/palpatine.png deleted file mode 100644 index ca10fe6..0000000 Binary files a/webroot/img/emoji/palpatine.png and /dev/null differ diff --git a/webroot/img/emoji/papalparrot.gif b/webroot/img/emoji/papalparrot.gif deleted file mode 100644 index d299237..0000000 Binary files a/webroot/img/emoji/papalparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/parrot.gif b/webroot/img/emoji/parrot.gif deleted file mode 100644 index b8c261a..0000000 Binary files a/webroot/img/emoji/parrot.gif and /dev/null differ diff --git a/webroot/img/emoji/parrotnotfound.gif b/webroot/img/emoji/parrotnotfound.gif deleted file mode 100644 index a039df9..0000000 Binary files a/webroot/img/emoji/parrotnotfound.gif and /dev/null differ diff --git a/webroot/img/emoji/partyparrot.gif b/webroot/img/emoji/partyparrot.gif deleted file mode 100644 index b88ecc4..0000000 Binary files a/webroot/img/emoji/partyparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/phparrot.gif b/webroot/img/emoji/phparrot.gif deleted file mode 100644 index 95e33b4..0000000 Binary files a/webroot/img/emoji/phparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/pirateparrot.gif b/webroot/img/emoji/pirateparrot.gif deleted file mode 100644 index 028848a..0000000 Binary files a/webroot/img/emoji/pirateparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/pizzaparrot.gif b/webroot/img/emoji/pizzaparrot.gif deleted file mode 100644 index 56d9dfc..0000000 Binary files a/webroot/img/emoji/pizzaparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/pokeparrot.gif b/webroot/img/emoji/pokeparrot.gif deleted file mode 100644 index a9adc86..0000000 Binary files a/webroot/img/emoji/pokeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/popcornparrot.gif b/webroot/img/emoji/popcornparrot.gif deleted file mode 100644 index 65b8585..0000000 Binary files a/webroot/img/emoji/popcornparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/porg.png b/webroot/img/emoji/porg.png deleted file mode 100644 index 68d08bf..0000000 Binary files a/webroot/img/emoji/porg.png and /dev/null differ diff --git a/webroot/img/emoji/portalparrot.gif b/webroot/img/emoji/portalparrot.gif deleted file mode 100644 index 5971fbd..0000000 Binary files a/webroot/img/emoji/portalparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/pumpkinparrot.gif b/webroot/img/emoji/pumpkinparrot.gif deleted file mode 100644 index f453ce2..0000000 Binary files a/webroot/img/emoji/pumpkinparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/quadparrot.gif b/webroot/img/emoji/quadparrot.gif deleted file mode 100644 index 9f1e319..0000000 Binary files a/webroot/img/emoji/quadparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/r2d2.png b/webroot/img/emoji/r2d2.png deleted file mode 100644 index 0a7fa09..0000000 Binary files a/webroot/img/emoji/r2d2.png and /dev/null differ diff --git a/webroot/img/emoji/redenvelopeparrot.gif b/webroot/img/emoji/redenvelopeparrot.gif deleted file mode 100644 index b40c76c..0000000 Binary files a/webroot/img/emoji/redenvelopeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/ripparrot.gif b/webroot/img/emoji/ripparrot.gif deleted file mode 100644 index 164250e..0000000 Binary files a/webroot/img/emoji/ripparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/rotatingparrot.gif b/webroot/img/emoji/rotatingparrot.gif deleted file mode 100644 index 1916cd0..0000000 Binary files a/webroot/img/emoji/rotatingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/ryangoslingparrot.gif b/webroot/img/emoji/ryangoslingparrot.gif deleted file mode 100644 index e241578..0000000 Binary files a/webroot/img/emoji/ryangoslingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/rythmicalparrot.gif b/webroot/img/emoji/rythmicalparrot.gif deleted file mode 100644 index db3e52d..0000000 Binary files a/webroot/img/emoji/rythmicalparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/sadparrot.gif b/webroot/img/emoji/sadparrot.gif deleted file mode 100644 index 25b500d..0000000 Binary files a/webroot/img/emoji/sadparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/schnitzelparrot.gif b/webroot/img/emoji/schnitzelparrot.gif deleted file mode 100644 index 00d22d7..0000000 Binary files a/webroot/img/emoji/schnitzelparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/scienceparrot.gif b/webroot/img/emoji/scienceparrot.gif deleted file mode 100644 index b5e4342..0000000 Binary files a/webroot/img/emoji/scienceparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/shipitparrot.gif b/webroot/img/emoji/shipitparrot.gif deleted file mode 100644 index 8d1ed30..0000000 Binary files a/webroot/img/emoji/shipitparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/shufflepartyparrot.gif b/webroot/img/emoji/shufflepartyparrot.gif deleted file mode 100644 index 7e754cb..0000000 Binary files a/webroot/img/emoji/shufflepartyparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/sintparrot.gif b/webroot/img/emoji/sintparrot.gif deleted file mode 100644 index 26f083b..0000000 Binary files a/webroot/img/emoji/sintparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/sithparrot.gif b/webroot/img/emoji/sithparrot.gif deleted file mode 100644 index df8f66e..0000000 Binary files a/webroot/img/emoji/sithparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/skiparrot.gif b/webroot/img/emoji/skiparrot.gif deleted file mode 100644 index 7cfa3c0..0000000 Binary files a/webroot/img/emoji/skiparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/sleepingparrot.gif b/webroot/img/emoji/sleepingparrot.gif deleted file mode 100644 index 6f0b6dd..0000000 Binary files a/webroot/img/emoji/sleepingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/sonic.gif b/webroot/img/emoji/sonic.gif deleted file mode 100644 index e9a9f4b..0000000 Binary files a/webroot/img/emoji/sonic.gif and /dev/null differ diff --git a/webroot/img/emoji/spyparrot.gif b/webroot/img/emoji/spyparrot.gif deleted file mode 100644 index 031b64e..0000000 Binary files a/webroot/img/emoji/spyparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/stalkerparrot.gif b/webroot/img/emoji/stalkerparrot.gif deleted file mode 100644 index 349d649..0000000 Binary files a/webroot/img/emoji/stalkerparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/starwars.png b/webroot/img/emoji/starwars.png deleted file mode 100644 index cef6546..0000000 Binary files a/webroot/img/emoji/starwars.png and /dev/null differ diff --git a/webroot/img/emoji/stayhomeparrot.gif b/webroot/img/emoji/stayhomeparrot.gif deleted file mode 100644 index 800adaf..0000000 Binary files a/webroot/img/emoji/stayhomeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/storm_trooper.gif b/webroot/img/emoji/storm_trooper.gif deleted file mode 100644 index 0f66de7..0000000 Binary files a/webroot/img/emoji/storm_trooper.gif and /dev/null differ diff --git a/webroot/img/emoji/stormtrooper.png b/webroot/img/emoji/stormtrooper.png deleted file mode 100644 index ae95358..0000000 Binary files a/webroot/img/emoji/stormtrooper.png and /dev/null differ diff --git a/webroot/img/emoji/sushiparrot.gif b/webroot/img/emoji/sushiparrot.gif deleted file mode 100644 index 2722018..0000000 Binary files a/webroot/img/emoji/sushiparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/tacoparrot.gif b/webroot/img/emoji/tacoparrot.gif deleted file mode 100644 index aed1d15..0000000 Binary files a/webroot/img/emoji/tacoparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/tennisparrot.gif b/webroot/img/emoji/tennisparrot.gif deleted file mode 100644 index dd992b6..0000000 Binary files a/webroot/img/emoji/tennisparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/thanks.png b/webroot/img/emoji/thanks.png deleted file mode 100644 index 80e0d32..0000000 Binary files a/webroot/img/emoji/thanks.png and /dev/null differ diff --git a/webroot/img/emoji/thumbsupparrot.gif b/webroot/img/emoji/thumbsupparrot.gif deleted file mode 100644 index df37921..0000000 Binary files a/webroot/img/emoji/thumbsupparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/tiedyeparrot.gif b/webroot/img/emoji/tiedyeparrot.gif deleted file mode 100644 index f5bff7b..0000000 Binary files a/webroot/img/emoji/tiedyeparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/tpparrot.gif b/webroot/img/emoji/tpparrot.gif deleted file mode 100644 index a647384..0000000 Binary files a/webroot/img/emoji/tpparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/transparront.gif b/webroot/img/emoji/transparront.gif deleted file mode 100644 index f9ecf8b..0000000 Binary files a/webroot/img/emoji/transparront.gif and /dev/null differ diff --git a/webroot/img/emoji/twinsparrot.gif b/webroot/img/emoji/twinsparrot.gif deleted file mode 100644 index c503979..0000000 Binary files a/webroot/img/emoji/twinsparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/upvoteparrot.gif b/webroot/img/emoji/upvoteparrot.gif deleted file mode 100644 index f4c8fb4..0000000 Binary files a/webroot/img/emoji/upvoteparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/vikingparrot.gif b/webroot/img/emoji/vikingparrot.gif deleted file mode 100644 index 049e112..0000000 Binary files a/webroot/img/emoji/vikingparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/wesmart.png b/webroot/img/emoji/wesmart.png deleted file mode 100644 index 5676faf..0000000 Binary files a/webroot/img/emoji/wesmart.png and /dev/null differ diff --git a/webroot/img/emoji/wfhparrot.gif b/webroot/img/emoji/wfhparrot.gif deleted file mode 100644 index 02469db..0000000 Binary files a/webroot/img/emoji/wfhparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/wineparrot.gif b/webroot/img/emoji/wineparrot.gif deleted file mode 100644 index e726f3c..0000000 Binary files a/webroot/img/emoji/wineparrot.gif and /dev/null differ diff --git a/webroot/img/emoji/yoda.gif b/webroot/img/emoji/yoda.gif deleted file mode 100644 index 117958c..0000000 Binary files a/webroot/img/emoji/yoda.gif and /dev/null differ diff --git a/webroot/img/favicon/android-icon-144x144.png b/webroot/img/favicon/android-icon-144x144.png deleted file mode 100644 index 3823379..0000000 Binary files a/webroot/img/favicon/android-icon-144x144.png and /dev/null differ diff --git a/webroot/img/favicon/android-icon-192x192.png b/webroot/img/favicon/android-icon-192x192.png deleted file mode 100644 index 551bf6c..0000000 Binary files a/webroot/img/favicon/android-icon-192x192.png and /dev/null differ diff --git a/webroot/img/favicon/android-icon-36x36.png b/webroot/img/favicon/android-icon-36x36.png deleted file mode 100644 index e8f248b..0000000 Binary files a/webroot/img/favicon/android-icon-36x36.png and /dev/null differ diff --git a/webroot/img/favicon/android-icon-48x48.png b/webroot/img/favicon/android-icon-48x48.png deleted file mode 100644 index 8b5e5e5..0000000 Binary files a/webroot/img/favicon/android-icon-48x48.png and /dev/null differ diff --git a/webroot/img/favicon/android-icon-72x72.png b/webroot/img/favicon/android-icon-72x72.png deleted file mode 100644 index 2f168d5..0000000 Binary files a/webroot/img/favicon/android-icon-72x72.png and /dev/null differ diff --git a/webroot/img/favicon/android-icon-96x96.png b/webroot/img/favicon/android-icon-96x96.png deleted file mode 100644 index 12f808d..0000000 Binary files a/webroot/img/favicon/android-icon-96x96.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-114x114.png b/webroot/img/favicon/apple-icon-114x114.png deleted file mode 100644 index 43f4f57..0000000 Binary files a/webroot/img/favicon/apple-icon-114x114.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-120x120.png b/webroot/img/favicon/apple-icon-120x120.png deleted file mode 100644 index a887d08..0000000 Binary files a/webroot/img/favicon/apple-icon-120x120.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-144x144.png b/webroot/img/favicon/apple-icon-144x144.png deleted file mode 100644 index 3823379..0000000 Binary files a/webroot/img/favicon/apple-icon-144x144.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-152x152.png b/webroot/img/favicon/apple-icon-152x152.png deleted file mode 100644 index caa3e0a..0000000 Binary files a/webroot/img/favicon/apple-icon-152x152.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-180x180.png b/webroot/img/favicon/apple-icon-180x180.png deleted file mode 100644 index f357367..0000000 Binary files a/webroot/img/favicon/apple-icon-180x180.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-57x57.png b/webroot/img/favicon/apple-icon-57x57.png deleted file mode 100644 index 1627d84..0000000 Binary files a/webroot/img/favicon/apple-icon-57x57.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-60x60.png b/webroot/img/favicon/apple-icon-60x60.png deleted file mode 100644 index f269199..0000000 Binary files a/webroot/img/favicon/apple-icon-60x60.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-72x72.png b/webroot/img/favicon/apple-icon-72x72.png deleted file mode 100644 index 2f168d5..0000000 Binary files a/webroot/img/favicon/apple-icon-72x72.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-76x76.png b/webroot/img/favicon/apple-icon-76x76.png deleted file mode 100644 index 00ce560..0000000 Binary files a/webroot/img/favicon/apple-icon-76x76.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon-precomposed.png b/webroot/img/favicon/apple-icon-precomposed.png deleted file mode 100644 index 2602bc6..0000000 Binary files a/webroot/img/favicon/apple-icon-precomposed.png and /dev/null differ diff --git a/webroot/img/favicon/apple-icon.png b/webroot/img/favicon/apple-icon.png deleted file mode 100644 index cc574a6..0000000 Binary files a/webroot/img/favicon/apple-icon.png and /dev/null differ diff --git a/webroot/img/favicon/browserconfig.xml b/webroot/img/favicon/browserconfig.xml deleted file mode 100644 index c554148..0000000 --- a/webroot/img/favicon/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff \ No newline at end of file diff --git a/webroot/img/favicon/favicon-16x16.png b/webroot/img/favicon/favicon-16x16.png deleted file mode 100644 index 4b544d3..0000000 Binary files a/webroot/img/favicon/favicon-16x16.png and /dev/null differ diff --git a/webroot/img/favicon/favicon-32x32.png b/webroot/img/favicon/favicon-32x32.png deleted file mode 100644 index a7f1de9..0000000 Binary files a/webroot/img/favicon/favicon-32x32.png and /dev/null differ diff --git a/webroot/img/favicon/favicon-96x96.png b/webroot/img/favicon/favicon-96x96.png deleted file mode 100644 index 12f808d..0000000 Binary files a/webroot/img/favicon/favicon-96x96.png and /dev/null differ diff --git a/webroot/img/favicon/ms-icon-144x144.png b/webroot/img/favicon/ms-icon-144x144.png deleted file mode 100644 index 3823379..0000000 Binary files a/webroot/img/favicon/ms-icon-144x144.png and /dev/null differ diff --git a/webroot/img/favicon/ms-icon-150x150.png b/webroot/img/favicon/ms-icon-150x150.png deleted file mode 100644 index 2f7a020..0000000 Binary files a/webroot/img/favicon/ms-icon-150x150.png and /dev/null differ diff --git a/webroot/img/favicon/ms-icon-310x310.png b/webroot/img/favicon/ms-icon-310x310.png deleted file mode 100644 index 18f1c72..0000000 Binary files a/webroot/img/favicon/ms-icon-310x310.png and /dev/null differ diff --git a/webroot/img/favicon/ms-icon-70x70.png b/webroot/img/favicon/ms-icon-70x70.png deleted file mode 100644 index a371c1f..0000000 Binary files a/webroot/img/favicon/ms-icon-70x70.png and /dev/null differ diff --git a/webroot/img/fediverse-black.png b/webroot/img/fediverse-black.png deleted file mode 100644 index 1bc6a79..0000000 Binary files a/webroot/img/fediverse-black.png and /dev/null differ diff --git a/webroot/img/fediverse-color.png b/webroot/img/fediverse-color.png deleted file mode 100644 index bb19584..0000000 Binary files a/webroot/img/fediverse-color.png and /dev/null differ diff --git a/webroot/img/fediverse-white.png b/webroot/img/fediverse-white.png deleted file mode 100644 index 63e770f..0000000 Binary files a/webroot/img/fediverse-white.png and /dev/null differ diff --git a/webroot/img/follow.svg b/webroot/img/follow.svg deleted file mode 100644 index ba904f1..0000000 --- a/webroot/img/follow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/hide-message-grey.svg b/webroot/img/hide-message-grey.svg deleted file mode 100644 index a09ee15..0000000 --- a/webroot/img/hide-message-grey.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/hide-message.svg b/webroot/img/hide-message.svg deleted file mode 100644 index 1d2c562..0000000 --- a/webroot/img/hide-message.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/like.svg b/webroot/img/like.svg deleted file mode 100644 index aa9e5f3..0000000 --- a/webroot/img/like.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/loading.gif b/webroot/img/loading.gif deleted file mode 100644 index b6ae174..0000000 Binary files a/webroot/img/loading.gif and /dev/null differ diff --git a/webroot/img/logo.png b/webroot/img/logo.png deleted file mode 100644 index fa10960..0000000 Binary files a/webroot/img/logo.png and /dev/null differ diff --git a/webroot/img/logo.svg b/webroot/img/logo.svg deleted file mode 100644 index ac3f9b7..0000000 --- a/webroot/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/menu-filled.svg b/webroot/img/menu-filled.svg deleted file mode 100644 index db16ab7..0000000 --- a/webroot/img/menu-filled.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/menu-vert.svg b/webroot/img/menu-vert.svg deleted file mode 100644 index 1a1876e..0000000 --- a/webroot/img/menu-vert.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/menu.svg b/webroot/img/menu.svg deleted file mode 100644 index 1e52e02..0000000 --- a/webroot/img/menu.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/moderator-grey.svg b/webroot/img/moderator-grey.svg deleted file mode 100644 index 1fa27fc..0000000 --- a/webroot/img/moderator-grey.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/moderator-nobackground.svg b/webroot/img/moderator-nobackground.svg deleted file mode 100644 index edc18fe..0000000 --- a/webroot/img/moderator-nobackground.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/moderator.svg b/webroot/img/moderator.svg deleted file mode 100644 index 24940eb..0000000 --- a/webroot/img/moderator.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/bandcamp.svg b/webroot/img/platformlogos/bandcamp.svg deleted file mode 100644 index 11764f4..0000000 --- a/webroot/img/platformlogos/bandcamp.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/default.svg b/webroot/img/platformlogos/default.svg deleted file mode 100644 index 85d53e9..0000000 --- a/webroot/img/platformlogos/default.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/discord.svg b/webroot/img/platformlogos/discord.svg deleted file mode 100644 index da184ae..0000000 --- a/webroot/img/platformlogos/discord.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/donate.svg b/webroot/img/platformlogos/donate.svg deleted file mode 100644 index 42f2772..0000000 --- a/webroot/img/platformlogos/donate.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/facebook.svg b/webroot/img/platformlogos/facebook.svg deleted file mode 100644 index 5517d3d..0000000 --- a/webroot/img/platformlogos/facebook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/follow.svg b/webroot/img/platformlogos/follow.svg deleted file mode 100644 index f988deb..0000000 --- a/webroot/img/platformlogos/follow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/github.svg b/webroot/img/platformlogos/github.svg deleted file mode 100644 index 3a8245b..0000000 --- a/webroot/img/platformlogos/github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/gitlab.svg b/webroot/img/platformlogos/gitlab.svg deleted file mode 100644 index 1a766a7..0000000 --- a/webroot/img/platformlogos/gitlab.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/google.svg b/webroot/img/platformlogos/google.svg deleted file mode 100644 index 96da1ef..0000000 --- a/webroot/img/platformlogos/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/instagram.svg b/webroot/img/platformlogos/instagram.svg deleted file mode 100644 index fd3fe9f..0000000 --- a/webroot/img/platformlogos/instagram.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/keyoxide.png b/webroot/img/platformlogos/keyoxide.png deleted file mode 100644 index d4476f4..0000000 Binary files a/webroot/img/platformlogos/keyoxide.png and /dev/null differ diff --git a/webroot/img/platformlogos/ko-fi.svg b/webroot/img/platformlogos/ko-fi.svg deleted file mode 100644 index 1e53bd6..0000000 --- a/webroot/img/platformlogos/ko-fi.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/lbry.svg b/webroot/img/platformlogos/lbry.svg deleted file mode 100644 index cc26f9a..0000000 --- a/webroot/img/platformlogos/lbry.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/liberapay.svg b/webroot/img/platformlogos/liberapay.svg deleted file mode 100644 index e124109..0000000 --- a/webroot/img/platformlogos/liberapay.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/link.svg b/webroot/img/platformlogos/link.svg deleted file mode 100644 index 5f21fe2..0000000 --- a/webroot/img/platformlogos/link.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/linkedin.svg b/webroot/img/platformlogos/linkedin.svg deleted file mode 100644 index e309996..0000000 --- a/webroot/img/platformlogos/linkedin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/mastodon.svg b/webroot/img/platformlogos/mastodon.svg deleted file mode 100644 index c5bc4af..0000000 --- a/webroot/img/platformlogos/mastodon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/odysee.svg b/webroot/img/platformlogos/odysee.svg deleted file mode 100644 index a900c51..0000000 --- a/webroot/img/platformlogos/odysee.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/patreon.svg b/webroot/img/platformlogos/patreon.svg deleted file mode 100644 index 5d8671c..0000000 --- a/webroot/img/platformlogos/patreon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/paypal.svg b/webroot/img/platformlogos/paypal.svg deleted file mode 100644 index dbc668a..0000000 --- a/webroot/img/platformlogos/paypal.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/snapchat.svg b/webroot/img/platformlogos/snapchat.svg deleted file mode 100644 index 6371b2a..0000000 --- a/webroot/img/platformlogos/snapchat.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/soundcloud.svg b/webroot/img/platformlogos/soundcloud.svg deleted file mode 100644 index 9f52826..0000000 --- a/webroot/img/platformlogos/soundcloud.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/spotify.svg b/webroot/img/platformlogos/spotify.svg deleted file mode 100644 index d5b89c7..0000000 --- a/webroot/img/platformlogos/spotify.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/steam.svg b/webroot/img/platformlogos/steam.svg deleted file mode 100644 index 712df4c..0000000 --- a/webroot/img/platformlogos/steam.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/tiktok.svg b/webroot/img/platformlogos/tiktok.svg deleted file mode 100644 index b1da38f..0000000 --- a/webroot/img/platformlogos/tiktok.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/twitch.svg b/webroot/img/platformlogos/twitch.svg deleted file mode 100644 index 052a086..0000000 --- a/webroot/img/platformlogos/twitch.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/twitter.svg b/webroot/img/platformlogos/twitter.svg deleted file mode 100644 index 3dedf3e..0000000 --- a/webroot/img/platformlogos/twitter.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/platformlogos/youtube.svg b/webroot/img/platformlogos/youtube.svg deleted file mode 100644 index 5741120..0000000 --- a/webroot/img/platformlogos/youtube.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/repost.svg b/webroot/img/repost.svg deleted file mode 100644 index 45d03a8..0000000 --- a/webroot/img/repost.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webroot/img/smiley.png b/webroot/img/smiley.png deleted file mode 100644 index 5d81490..0000000 Binary files a/webroot/img/smiley.png and /dev/null differ diff --git a/webroot/img/user-icon.svg b/webroot/img/user-icon.svg deleted file mode 100644 index 09dcacb..0000000 --- a/webroot/img/user-icon.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/webroot/index-standalone-chat-readonly.html b/webroot/index-standalone-chat-readonly.html deleted file mode 100644 index e1e9d42..0000000 --- a/webroot/index-standalone-chat-readonly.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - -
- - - - diff --git a/webroot/index-standalone-chat-readwrite.html b/webroot/index-standalone-chat-readwrite.html deleted file mode 100644 index 159454c..0000000 --- a/webroot/index-standalone-chat-readwrite.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - -
- - - - diff --git a/webroot/index-standalone-chat.html b/webroot/index-standalone-chat.html deleted file mode 120000 index bba6dc0..0000000 --- a/webroot/index-standalone-chat.html +++ /dev/null @@ -1 +0,0 @@ -index-standalone-chat-readonly.html \ No newline at end of file diff --git a/webroot/index-video-only.html b/webroot/index-video-only.html deleted file mode 100644 index c02acd6..0000000 --- a/webroot/index-video-only.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - -
- - - - diff --git a/webroot/index.html b/webroot/index.html deleted file mode 100644 index 469c181..0000000 --- a/webroot/index.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - Owncast - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
- - - - - - - diff --git a/webroot/js/app-standalone-chat.js b/webroot/js/app-standalone-chat.js deleted file mode 100644 index 18663d6..0000000 --- a/webroot/js/app-standalone-chat.js +++ /dev/null @@ -1,337 +0,0 @@ -import { h, Component } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); -import UsernameForm from './components/chat/username.js'; -import Chat from './components/chat/chat.js'; -import Websocket, { - CALLBACKS, - SOCKET_MESSAGE_TYPES, -} from './utils/websocket.js'; -import { registerChat } from './chat/register.js'; -import { getLocalStorage, setLocalStorage } from './utils/helpers.js'; -import { - CHAT_MAX_MESSAGE_LENGTH, - EST_SOCKET_PAYLOAD_BUFFER, - KEY_EMBED_CHAT_ACCESS_TOKEN, - KEY_ACCESS_TOKEN, - KEY_USERNAME, - TIMER_DISABLE_CHAT_AFTER_OFFLINE, - URL_STATUS, - URL_CONFIG, - TIMER_STATUS_UPDATE, -} from './utils/constants.js'; - -export default class StandaloneChat extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - websocket: null, - canChat: false, - chatEnabled: true, // always true for standalone chat - chatInputEnabled: false, // chat input box state - accessToken: null, - username: null, - isRegistering: false, - streamOnline: null, // stream is active/online - lastDisconnectTime: null, - configData: { - loading: true, - }, - }; - this.disableChatInputTimer = null; - this.hasConfiguredChat = false; - - this.handleUsernameChange = this.handleUsernameChange.bind(this); - this.handleOfflineMode = this.handleOfflineMode.bind(this); - this.handleOnlineMode = this.handleOnlineMode.bind(this); - this.handleFormFocus = this.handleFormFocus.bind(this); - this.handleFormBlur = this.handleFormBlur.bind(this); - this.getStreamStatus = this.getStreamStatus.bind(this); - this.getConfig = this.getConfig.bind(this); - this.disableChatInput = this.disableChatInput.bind(this); - this.setupChatAuth = this.setupChatAuth.bind(this); - this.disableChat = this.disableChat.bind(this); - - // user events - this.handleWebsocketMessage = this.handleWebsocketMessage.bind(this); - - this.getConfig(); - - this.getStreamStatus(); - this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); - } - - // fetch /config data - getConfig() { - fetch(URL_CONFIG) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - this.setConfigData(json); - }) - .catch((error) => { - this.handleNetworkingError(`Fetch config: ${error}`); - }); - } - - // fetch stream status - getStreamStatus() { - fetch(URL_STATUS) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - this.updateStreamStatus(json); - }) - .catch((error) => { - this.handleOfflineMode(); - this.handleNetworkingError(`Stream status: ${error}`); - }); - } - - setConfigData(data = {}) { - const { chatDisabled } = data; - - // If this is the first time setting the config - // then setup chat if it's enabled. - if (!this.hasConfiguredChat && !chatDisabled) { - this.setupChatAuth(); - } - - this.hasConfiguredChat = true; - - this.setState({ - canChat: !chatDisabled, - configData: { - ...data, - }, - }); - } - - // handle UI things from stream status result - updateStreamStatus(status = {}) { - const { streamOnline: curStreamOnline } = this.state; - - if (!status) { - return; - } - const { online, lastDisconnectTime } = status; - - this.setState({ - lastDisconnectTime, - streamOnline: online, - }); - - if (status.online !== curStreamOnline) { - if (status.online) { - // stream has just come online. - this.handleOnlineMode(); - } else { - // stream has just flipped offline or app just got loaded and stream is offline. - this.handleOfflineMode(lastDisconnectTime); - } - } - } - - // stop status timer and disable chat after some time. - handleOfflineMode(lastDisconnectTime) { - if (lastDisconnectTime) { - const remainingChatTime = - TIMER_DISABLE_CHAT_AFTER_OFFLINE - - (Date.now() - new Date(lastDisconnectTime)); - const countdown = remainingChatTime < 0 ? 0 : remainingChatTime; - if (countdown > 0) { - this.setState({ - chatInputEnabled: true, - }); - } - this.disableChatInputTimer = setTimeout(this.disableChatInput, countdown); - } - this.setState({ - streamOnline: false, - }); - } - - handleOnlineMode() { - clearTimeout(this.disableChatInputTimer); - this.disableChatInputTimer = null; - - this.setState({ - streamOnline: true, - chatInputEnabled: true, - }); - } - - handleUsernameChange(newName) { - this.setState({ - username: newName, - }); - this.sendUsernameChange(newName); - } - - disableChatInput() { - this.setState({ - chatInputEnabled: false, - }); - } - - handleNetworkingError(error) { - console.error(`>>> App Error: ${error}`); - } - - handleWebsocketMessage(e) { - if (e.type === SOCKET_MESSAGE_TYPES.ERROR_USER_DISABLED) { - // User has been actively disabled on the backend. Turn off chat for them. - this.handleBlockedChat(); - } else if ( - e.type === SOCKET_MESSAGE_TYPES.ERROR_NEEDS_REGISTRATION && - !this.isRegistering - ) { - // User needs an access token, so start the user auth flow. - this.state.websocket.shutdown(); - this.setState({ websocket: null }); - this.setupChatAuth(true); - } else if (e.type === SOCKET_MESSAGE_TYPES.ERROR_MAX_CONNECTIONS_EXCEEDED) { - // Chat server cannot support any more chat clients. Turn off chat for them. - this.disableChat(); - } else if (e.type === SOCKET_MESSAGE_TYPES.CONNECTED_USER_INFO) { - // When connected the user will return an event letting us know what our - // user details are so we can display them properly. - const { user } = e; - const { displayName } = user; - - this.setState({ username: displayName }); - } - } - - handleBlockedChat() { - setLocalStorage('owncast_chat_blocked', true); - this.disableChat(); - } - - handleFormFocus() { - if (this.hasTouchScreen) { - this.setState({ - touchKeyboardActive: true, - }); - } - } - - handleFormBlur() { - if (this.hasTouchScreen) { - this.setState({ - touchKeyboardActive: false, - }); - } - } - - disableChat() { - this.state.websocket.shutdown(); - this.setState({ websocket: null, canChat: false }); - } - - async setupChatAuth(force) { - const { readonly } = this.props; - var accessToken = readonly - ? getLocalStorage(KEY_EMBED_CHAT_ACCESS_TOKEN) - : getLocalStorage(KEY_ACCESS_TOKEN); - var randomIntArray = new Uint32Array(1); - window.crypto.getRandomValues(randomIntArray); - var username = readonly - ? 'chat-embed-' + randomIntArray[0] - : getLocalStorage(KEY_USERNAME); - - if (!accessToken || force) { - try { - this.isRegistering = true; - const registration = await registerChat(username); - accessToken = registration.accessToken; - username = registration.displayName; - - if (readonly) { - setLocalStorage(KEY_EMBED_CHAT_ACCESS_TOKEN, accessToken); - } else { - setLocalStorage(KEY_ACCESS_TOKEN, accessToken); - setLocalStorage(KEY_USERNAME, username); - } - - this.isRegistering = false; - } catch (e) { - console.error('registration error:', e); - } - } - - if (this.state.websocket) { - this.state.websocket.shutdown(); - this.setState({ - websocket: null, - }); - } - - // Without a valid access token he websocket connection will be rejected. - const websocket = new Websocket(accessToken); - websocket.addListener( - CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, - this.handleWebsocketMessage - ); - - this.setState({ - username, - websocket, - accessToken, - }); - } - - sendUsernameChange(newName) { - const nameChange = { - type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, - newName, - }; - this.state.websocket.send(nameChange); - } - - render(props, state) { - const { username, websocket, accessToken, chatInputEnabled, configData } = - state; - - const { chatDisabled, maxSocketPayloadSize, customStyles, name } = - configData; - - const { readonly } = props; - return this.state.websocket - ? html`${!readonly - ? html` -
- <${UsernameForm} - username=${username} - onUsernameChange=${this.handleUsernameChange} - onFocus=${this.handleFormFocus} - onBlur=${this.handleFormBlur} - /> -
` - : ''} - <${Chat} - websocket=${websocket} - username=${username} - accessToken=${accessToken} - readonly=${readonly} - instanceTitle=${name} - chatInputEnabled=${chatInputEnabled && !chatDisabled} - inputMaxBytes=${maxSocketPayloadSize - EST_SOCKET_PAYLOAD_BUFFER || - CHAT_MAX_MESSAGE_LENGTH} - />` - : null; - } -} diff --git a/webroot/js/app-video-only.js b/webroot/js/app-video-only.js deleted file mode 100644 index cc618ef..0000000 --- a/webroot/js/app-video-only.js +++ /dev/null @@ -1,287 +0,0 @@ -import { h, Component } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); - -import VideoPoster from './components/video-poster.js'; -import { OwncastPlayer } from './components/player.js'; - -import { - addNewlines, - makeLastOnlineString, - pluralize, - parseSecondsToDurationString, -} from './utils/helpers.js'; -import { - URL_CONFIG, - URL_STATUS, - URL_VIEWER_PING, - TIMER_STATUS_UPDATE, - TIMER_STREAM_DURATION_COUNTER, - TEMP_IMAGE, - MESSAGE_OFFLINE, - MESSAGE_ONLINE, -} from './utils/constants.js'; - -export default class VideoOnly extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - configData: {}, - - playerActive: false, // player object is active - streamOnline: false, // stream is active/online - - isPlaying: false, - - //status - streamStatusMessage: MESSAGE_OFFLINE, - viewerCount: '', - lastDisconnectTime: null, - }; - - // timers - this.playerRestartTimer = null; - this.offlineTimer = null; - this.statusTimer = null; - this.streamDurationTimer = null; - - this.handleOfflineMode = this.handleOfflineMode.bind(this); - this.handleOnlineMode = this.handleOnlineMode.bind(this); - this.setCurrentStreamDuration = this.setCurrentStreamDuration.bind(this); - - // player events - this.handlePlayerReady = this.handlePlayerReady.bind(this); - this.handlePlayerPlaying = this.handlePlayerPlaying.bind(this); - this.handlePlayerEnded = this.handlePlayerEnded.bind(this); - this.handlePlayerError = this.handlePlayerError.bind(this); - - // fetch events - this.getConfig = this.getConfig.bind(this); - this.getStreamStatus = this.getStreamStatus.bind(this); - } - - componentDidMount() { - this.getConfig(); - - this.player = new OwncastPlayer(); - this.player.setupPlayerCallbacks({ - onReady: this.handlePlayerReady, - onPlaying: this.handlePlayerPlaying, - onEnded: this.handlePlayerEnded, - onError: this.handlePlayerError, - }); - this.player.init(); - } - - componentWillUnmount() { - // clear all the timers - clearInterval(this.playerRestartTimer); - clearInterval(this.offlineTimer); - clearInterval(this.statusTimer); - clearInterval(this.streamDurationTimer); - } - - // fetch /config data - getConfig() { - fetch(URL_CONFIG) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - this.setConfigData(json); - }) - .catch((error) => { - this.handleNetworkingError(`Fetch config: ${error}`); - }); - } - - // fetch stream status - getStreamStatus() { - fetch(URL_STATUS) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - this.updateStreamStatus(json); - }) - .catch((error) => { - this.handleOfflineMode(); - this.handleNetworkingError(`Stream status: ${error}`); - }); - - // Ping the API to let them know we're an active viewer - fetch(URL_VIEWER_PING).catch((error) => { - this.handleOfflineMode(); - this.handleNetworkingError(`Viewer PING error: ${error}`); - }); - } - - setConfigData(data = {}) { - const { title, summary } = data; - window.document.title = title; - this.setState({ - configData: { - ...data, - summary: summary && addNewlines(summary), - }, - }); - } - - // handle UI things from stream status result - updateStreamStatus(status = {}) { - const { streamOnline: curStreamOnline } = this.state; - - if (!status) { - return; - } - const { viewerCount, online, lastConnectTime, lastDisconnectTime } = status; - - if (status.online && !curStreamOnline) { - // stream has just come online. - this.handleOnlineMode(); - } else if (!status.online && curStreamOnline) { - // stream has just flipped offline. - this.handleOfflineMode(); - } - this.setState({ - viewerCount, - streamOnline: online, - lastDisconnectTime, - lastConnectTime, - }); - } - - // when videojs player is ready, start polling for stream - handlePlayerReady() { - this.getStreamStatus(); - this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); - } - - handlePlayerPlaying() { - this.setState({ - isPlaying: true, - }); - } - - // likely called some time after stream status has gone offline. - // basically hide video and show underlying "poster" - handlePlayerEnded() { - this.setState({ - playerActive: false, - isPlaying: false, - }); - } - - handlePlayerError() { - // do something? - this.handleOfflineMode(); - this.handlePlayerEnded(); - } - - // stop status timer and disable chat after some time. - handleOfflineMode() { - clearInterval(this.streamDurationTimer); - this.setState({ - streamOnline: false, - streamStatusMessage: MESSAGE_OFFLINE, - }); - } - - setCurrentStreamDuration() { - let streamDurationString = ''; - if (this.state.lastConnectTime) { - const diff = (Date.now() - Date.parse(this.state.lastConnectTime)) / 1000; - streamDurationString = parseSecondsToDurationString(diff); - } - this.setState({ - streamStatusMessage: `${MESSAGE_ONLINE} ${streamDurationString}`, - }); - } - - // play video! - handleOnlineMode() { - this.player.startPlayer(); - - this.streamDurationTimer = setInterval( - this.setCurrentStreamDuration, - TIMER_STREAM_DURATION_COUNTER - ); - - this.setState({ - playerActive: true, - streamOnline: true, - streamStatusMessage: MESSAGE_ONLINE, - }); - } - - handleNetworkingError(error) { - console.error(`>>> App Error: ${error}`); - } - - render(props, state) { - const { - configData, - - viewerCount, - playerActive, - streamOnline, - streamStatusMessage, - lastDisconnectTime, - isPlaying, - } = state; - - const { logo = TEMP_IMAGE, customStyles } = configData; - - let viewerCountMessage = ''; - if (streamOnline && viewerCount > 0) { - viewerCountMessage = html`${viewerCount} - ${pluralize(' viewer', viewerCount)}`; - } else if (lastDisconnectTime) { - viewerCountMessage = makeLastOnlineString(lastDisconnectTime); - } - - const mainClass = playerActive ? 'online' : ''; - - const poster = isPlaying - ? null - : html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `; - return html` -
- -
- - ${poster} -
- -
- ${streamStatusMessage} - ${viewerCountMessage} -
-
- `; - } -} diff --git a/webroot/js/app.js b/webroot/js/app.js deleted file mode 100644 index 0cb6d71..0000000 --- a/webroot/js/app.js +++ /dev/null @@ -1,975 +0,0 @@ -import { h, Component } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); - -import { OwncastPlayer } from './components/player.js'; -import SocialIconsList from './components/platform-logos-list.js'; -import UsernameForm from './components/chat/username.js'; -import VideoPoster from './components/video-poster.js'; -import Followers from './components/federation/followers.js'; - -import Chat from './components/chat/chat.js'; -import Websocket, { - CALLBACKS, - SOCKET_MESSAGE_TYPES, -} from './utils/websocket.js'; -import { registerChat } from './chat/register.js'; - -import ExternalActionModal, { - ExternalActionButton, -} from './components/external-action-modal.js'; - -import FediverseFollowModal, { - FediverseFollowButton, -} from './components/fediverse-follow-modal.js'; - -import { - addNewlines, - checkUrlPathForDisplay, - classNames, - debounce, - getLocalStorage, - getOrientation, - hasTouchScreen, - makeLastOnlineString, - parseSecondsToDurationString, - pluralize, - ROUTE_RECORDINGS, - setLocalStorage, -} from './utils/helpers.js'; -import { - CHAT_MAX_MESSAGE_LENGTH, - EST_SOCKET_PAYLOAD_BUFFER, - HEIGHT_SHORT_WIDE, - KEY_ACCESS_TOKEN, - KEY_CHAT_DISPLAYED, - KEY_USERNAME, - MESSAGE_OFFLINE, - MESSAGE_ONLINE, - ORIENTATION_PORTRAIT, - OWNCAST_LOGO_LOCAL, - TEMP_IMAGE, - TIMER_DISABLE_CHAT_AFTER_OFFLINE, - TIMER_STATUS_UPDATE, - TIMER_STREAM_DURATION_COUNTER, - URL_CONFIG, - URL_OWNCAST, - URL_STATUS, - URL_VIEWER_PING, - WIDTH_SINGLE_COL, -} from './utils/constants.js'; -import { checkIsModerator } from './utils/chat.js'; -import TabBar from './components/tab-bar.js'; - -export default class App extends Component { - constructor(props, context) { - super(props, context); - - const chatStorage = getLocalStorage(KEY_CHAT_DISPLAYED); - this.hasTouchScreen = hasTouchScreen(); - this.windowBlurred = false; - - this.state = { - websocket: null, - canChat: false, // all of chat functionality (panel + username) - displayChatPanel: chatStorage === null ? true : chatStorage === 'true', // just the chat panel - chatInputEnabled: false, // chat input box state - accessToken: null, - username: getLocalStorage(KEY_USERNAME), - isModerator: false, - - isRegistering: false, - touchKeyboardActive: false, - - configData: { - loading: true, - }, - extraPageContent: '', - - playerActive: false, // player object is active - streamOnline: null, // stream is active/online - isPlaying: false, // player is actively playing video - - // status - streamStatusMessage: MESSAGE_OFFLINE, - viewerCount: '', - lastDisconnectTime: null, - - // dom - windowWidth: window.innerWidth, - windowHeight: window.innerHeight, - orientation: getOrientation(this.hasTouchScreen), - - // modals - externalActionModalData: null, - fediverseModalData: null, - - // routing & tabbing - section: '', - sectionId: '', - }; - - // timers - this.playerRestartTimer = null; - this.offlineTimer = null; - this.statusTimer = null; - this.disableChatInputTimer = null; - this.streamDurationTimer = null; - - // misc dom events - this.handleChatPanelToggle = this.handleChatPanelToggle.bind(this); - this.handleUsernameChange = this.handleUsernameChange.bind(this); - this.handleFormFocus = this.handleFormFocus.bind(this); - this.handleFormBlur = this.handleFormBlur.bind(this); - this.handleWindowBlur = this.handleWindowBlur.bind(this); - this.handleWindowFocus = this.handleWindowFocus.bind(this); - this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 250); - - this.handleOfflineMode = this.handleOfflineMode.bind(this); - this.handleOnlineMode = this.handleOnlineMode.bind(this); - this.disableChatInput = this.disableChatInput.bind(this); - this.setCurrentStreamDuration = this.setCurrentStreamDuration.bind(this); - - this.handleKeyPressed = this.handleKeyPressed.bind(this); - this.displayExternalAction = this.displayExternalAction.bind(this); - this.closeExternalActionModal = this.closeExternalActionModal.bind(this); - this.displayFediverseFollowModal = - this.displayFediverseFollowModal.bind(this); - this.closeFediverseFollowModal = this.closeFediverseFollowModal.bind(this); - - // player events - this.handlePlayerReady = this.handlePlayerReady.bind(this); - this.handlePlayerPlaying = this.handlePlayerPlaying.bind(this); - this.handlePlayerEnded = this.handlePlayerEnded.bind(this); - this.handlePlayerError = this.handlePlayerError.bind(this); - - // fetch events - this.getConfig = this.getConfig.bind(this); - this.getStreamStatus = this.getStreamStatus.bind(this); - - // user events - this.handleWebsocketMessage = this.handleWebsocketMessage.bind(this); - - // chat - this.hasConfiguredChat = false; - this.setupChatAuth = this.setupChatAuth.bind(this); - this.disableChat = this.disableChat.bind(this); - } - - componentDidMount() { - this.getConfig(); - if (!this.hasTouchScreen) { - window.addEventListener('resize', this.handleWindowResize); - } - window.addEventListener('blur', this.handleWindowBlur); - window.addEventListener('focus', this.handleWindowFocus); - if (this.hasTouchScreen) { - window.addEventListener('orientationchange', this.handleWindowResize); - } - window.addEventListener('keypress', this.handleKeyPressed); - this.player = new OwncastPlayer(); - this.player.setupPlayerCallbacks({ - onReady: this.handlePlayerReady, - onPlaying: this.handlePlayerPlaying, - onEnded: this.handlePlayerEnded, - onError: this.handlePlayerError, - }); - this.player.init(); - - // check routing - this.getRoute(); - } - - componentWillUnmount() { - // clear all the timers - clearInterval(this.playerRestartTimer); - clearInterval(this.offlineTimer); - clearInterval(this.statusTimer); - clearTimeout(this.disableChatInputTimer); - clearInterval(this.streamDurationTimer); - window.removeEventListener('resize', this.handleWindowResize); - window.removeEventListener('blur', this.handleWindowBlur); - window.removeEventListener('focus', this.handleWindowFocus); - window.removeEventListener('keypress', this.handleKeyPressed); - if (this.hasTouchScreen) { - window.removeEventListener('orientationchange', this.handleWindowResize); - } - } - - getRoute() { - const routeInfo = checkUrlPathForDisplay(); - this.setState({ - ...routeInfo, - }); - } - - // fetch /config data - getConfig() { - fetch(URL_CONFIG) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - this.setConfigData(json); - }) - .catch((error) => { - this.handleNetworkingError(`Fetch config: ${error}`); - }); - } - - // fetch stream status - getStreamStatus() { - fetch(URL_STATUS) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - this.updateStreamStatus(json); - }) - .catch((error) => { - this.handleOfflineMode(); - this.handleNetworkingError(`Stream status: ${error}`); - }); - - // Ping the API to let them know we're an active viewer - fetch(URL_VIEWER_PING).catch((error) => { - this.handleOfflineMode(); - this.handleNetworkingError(`Viewer PING error: ${error}`); - }); - } - - setConfigData(data = {}) { - const { name, summary, chatDisabled } = data; - window.document.title = name; - - // If this is the first time setting the config - // then setup chat if it's enabled. - if (!this.hasConfiguredChat && !chatDisabled) { - this.setupChatAuth(); - } - - this.hasConfiguredChat = true; - - this.setState({ - canChat: !chatDisabled, - configData: { - ...data, - summary: summary && addNewlines(summary), - }, - }); - } - - // handle UI things from stream status result - updateStreamStatus(status = {}) { - const { streamOnline: curStreamOnline } = this.state; - - if (!status) { - return; - } - const { - viewerCount, - online, - lastConnectTime, - streamTitle, - lastDisconnectTime, - } = status; - - this.setState({ - viewerCount, - lastConnectTime, - streamOnline: online, - streamTitle, - lastDisconnectTime, - }); - - if (status.online !== curStreamOnline) { - if (status.online) { - // stream has just come online. - this.handleOnlineMode(); - } else { - // stream has just flipped offline or app just got loaded and stream is offline. - this.handleOfflineMode(lastDisconnectTime); - } - } - } - - // when videojs player is ready, start polling for stream - handlePlayerReady() { - this.getStreamStatus(); - this.statusTimer = setInterval(this.getStreamStatus, TIMER_STATUS_UPDATE); - } - - handlePlayerPlaying() { - this.setState({ - isPlaying: true, - }); - } - - // likely called some time after stream status has gone offline. - // basically hide video and show underlying "poster" - handlePlayerEnded() { - this.setState({ - playerActive: false, - isPlaying: false, - }); - } - - handlePlayerError() { - // do something? - this.handleOfflineMode(); - this.handlePlayerEnded(); - } - - // stop status timer and disable chat after some time. - handleOfflineMode(lastDisconnectTime) { - clearInterval(this.streamDurationTimer); - - if (lastDisconnectTime) { - const remainingChatTime = - TIMER_DISABLE_CHAT_AFTER_OFFLINE - - (Date.now() - new Date(lastDisconnectTime)); - const countdown = remainingChatTime < 0 ? 0 : remainingChatTime; - if (countdown > 0) { - this.setState({ - chatInputEnabled: true, - }); - } - this.disableChatInputTimer = setTimeout(this.disableChatInput, countdown); - } - - this.setState({ - streamOnline: false, - streamStatusMessage: MESSAGE_OFFLINE, - }); - - if (this.player.vjsPlayer && this.player.vjsPlayer.paused()) { - this.handlePlayerEnded(); - } - - if (this.windowBlurred) { - document.title = ` 🔴 ${ - this.state.configData && this.state.configData.name - }`; - } - } - - // play video! - handleOnlineMode() { - this.player.startPlayer(); - clearTimeout(this.disableChatInputTimer); - this.disableChatInputTimer = null; - - this.streamDurationTimer = setInterval( - this.setCurrentStreamDuration, - TIMER_STREAM_DURATION_COUNTER - ); - - this.setState({ - playerActive: true, - streamOnline: true, - chatInputEnabled: true, - streamTitle: '', - streamStatusMessage: MESSAGE_ONLINE, - }); - - if (this.windowBlurred) { - document.title = ` 🟢 ${ - this.state.configData && this.state.configData.name - }`; - } - } - - setCurrentStreamDuration() { - let streamDurationString = ''; - if (this.state.lastConnectTime) { - const diff = (Date.now() - Date.parse(this.state.lastConnectTime)) / 1000; - streamDurationString = parseSecondsToDurationString(diff); - } - this.setState({ - streamStatusMessage: `${MESSAGE_ONLINE} ${streamDurationString}`, - }); - } - - handleUsernameChange(newName) { - this.setState({ - username: newName, - }); - - this.sendUsernameChange(newName); - } - - handleFormFocus() { - if (this.hasTouchScreen) { - this.setState({ - touchKeyboardActive: true, - }); - } - } - - handleFormBlur() { - if (this.hasTouchScreen) { - this.setState({ - touchKeyboardActive: false, - }); - } - } - - handleChatPanelToggle() { - const { displayChatPanel: curDisplayed } = this.state; - - const displayChat = !curDisplayed; - setLocalStorage(KEY_CHAT_DISPLAYED, displayChat); - this.setState({ - displayChatPanel: displayChat, - }); - } - - disableChatInput() { - this.setState({ - chatInputEnabled: false, - }); - } - - handleNetworkingError(error) { - console.error(`>>> App Error: ${error}`); - } - - handleWindowResize() { - this.setState({ - windowWidth: window.innerWidth, - windowHeight: window.innerHeight, - orientation: getOrientation(this.hasTouchScreen), - }); - } - - handleWindowBlur() { - this.windowBlurred = true; - } - - handleWindowFocus() { - this.windowBlurred = false; - window.document.title = this.state.configData && this.state.configData.name; - } - - handleSpaceBarPressed(e) { - e.preventDefault(); - if (this.state.isPlaying) { - this.setState({ - isPlaying: false, - }); - try { - this.player.vjsPlayer.pause(); - } catch (err) { - console.warn(err); - } - } else { - this.setState({ - isPlaying: true, - }); - this.player.vjsPlayer.play(); - } - } - - handleMuteKeyPressed() { - const muted = this.player.vjsPlayer.muted(); - const volume = this.player.vjsPlayer.volume(); - - try { - if (volume === 0) { - this.player.vjsPlayer.volume(0.5); - this.player.vjsPlayer.muted(false); - } else { - this.player.vjsPlayer.muted(!muted); - } - } catch (err) { - console.warn(err); - } - } - - handleFullScreenKeyPressed() { - if (this.player.vjsPlayer.isFullscreen()) { - this.player.vjsPlayer.exitFullscreen(); - } else { - this.player.vjsPlayer.requestFullscreen(); - } - } - - handleVolumeSet(factor) { - this.player.vjsPlayer.volume(this.player.vjsPlayer.volume() + factor); - } - - handleKeyPressed(e) { - // Only handle shortcuts if the focus is on the general page body, - // not a specific input field. - if (e.target !== document.getElementById('app-body')) { - return; - } - - if (this.state.streamOnline) { - switch (e.code) { - case 'MediaPlayPause': - case 'KeyP': - case 'Space': - this.handleSpaceBarPressed(e); - break; - case 'KeyM': - this.handleMuteKeyPressed(e); - break; - case 'KeyF': - this.handleFullScreenKeyPressed(e); - break; - case 'KeyC': - this.handleChatPanelToggle(); - break; - case 'Digit9': - this.handleVolumeSet(-0.1); - break; - case 'Digit0': - this.handleVolumeSet(0.1); - } - } - } - - displayExternalAction(action) { - const { username } = this.state; - if (!action) { - return; - } - const { url: actionUrl, openExternally } = action || {}; - let url = new URL(actionUrl); - // Append url and username to params so the link knows where we came from and who we are. - url.searchParams.append('username', username); - url.searchParams.append('instance', window.location); - - const fullUrl = url.toString(); - - if (openExternally) { - var win = window.open(fullUrl, '_blank'); - win.focus(); - return; - } - this.setState({ - externalActionModalData: { - ...action, - url: fullUrl, - }, - }); - } - closeExternalActionModal() { - this.setState({ - externalActionModalData: null, - }); - } - - displayFediverseFollowModal(data) { - this.setState({ fediverseModalData: data }); - } - closeFediverseFollowModal() { - this.setState({ fediverseModalData: null }); - } - - handleWebsocketMessage(e) { - if (e.type === SOCKET_MESSAGE_TYPES.ERROR_USER_DISABLED) { - // User has been actively disabled on the backend. Turn off chat for them. - this.handleBlockedChat(); - } else if ( - e.type === SOCKET_MESSAGE_TYPES.ERROR_NEEDS_REGISTRATION && - !this.isRegistering - ) { - // User needs an access token, so start the user auth flow. - this.state.websocket.shutdown(); - this.setState({ websocket: null }); - this.setupChatAuth(true); - } else if (e.type === SOCKET_MESSAGE_TYPES.ERROR_MAX_CONNECTIONS_EXCEEDED) { - // Chat server cannot support any more chat clients. Turn off chat for them. - this.disableChat(); - } else if (e.type === SOCKET_MESSAGE_TYPES.CONNECTED_USER_INFO) { - // When connected the user will return an event letting us know what our - // user details are so we can display them properly. - const { user } = e; - const { displayName } = user; - - this.setState({ - username: displayName, - isModerator: checkIsModerator(e), - }); - } - } - - handleBlockedChat() { - this.disableChat(); - } - - disableChat() { - this.state.websocket.shutdown(); - this.setState({ websocket: null, canChat: false }); - } - - async setupChatAuth(force) { - var accessToken = getLocalStorage(KEY_ACCESS_TOKEN); - var username = getLocalStorage(KEY_USERNAME); - - if (!accessToken || force) { - try { - this.isRegistering = true; - const registration = await registerChat(this.state.username); - accessToken = registration.accessToken; - username = registration.displayName; - - setLocalStorage(KEY_ACCESS_TOKEN, accessToken); - setLocalStorage(KEY_USERNAME, username); - - this.isRegistering = false; - } catch (e) { - console.error('registration error:', e); - } - } - - if (this.state.websocket) { - this.state.websocket.shutdown(); - this.setState({ - websocket: null, - }); - } - - // Without a valid access token he websocket connection will be rejected. - const websocket = new Websocket(accessToken); - websocket.addListener( - CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, - this.handleWebsocketMessage - ); - - this.setState({ - username, - websocket, - accessToken, - }); - } - - sendUsernameChange(newName) { - const nameChange = { - type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, - newName, - }; - this.state.websocket.send(nameChange); - } - - render(props, state) { - const { - chatInputEnabled, - configData, - displayChatPanel, - canChat, - isModerator, - - isPlaying, - orientation, - playerActive, - streamOnline, - streamStatusMessage, - streamTitle, - touchKeyboardActive, - username, - viewerCount, - websocket, - windowHeight, - windowWidth, - fediverseModalData, - externalActionModalData, - lastDisconnectTime, - section, - sectionId, - } = state; - - const { - version: appVersion, - logo = TEMP_IMAGE, - socialHandles = [], - summary, - tags = [], - name, - extraPageContent, - chatDisabled, - externalActions, - customStyles, - maxSocketPayloadSize, - federation = {}, - } = configData; - - const bgUserLogo = { backgroundImage: `url(${logo})` }; - - const tagList = tags !== null && tags.length > 0 && tags.join(' #'); - - let viewerCountMessage = ''; - if (streamOnline && viewerCount > 0) { - viewerCountMessage = html`${viewerCount} - ${pluralize(' viewer', viewerCount)}`; - } else if (lastDisconnectTime) { - viewerCountMessage = makeLastOnlineString(lastDisconnectTime); - } - - const mainClass = playerActive ? 'online' : ''; - const isPortrait = - this.hasTouchScreen && orientation === ORIENTATION_PORTRAIT; - const shortHeight = windowHeight <= HEIGHT_SHORT_WIDE && !isPortrait; - const singleColMode = windowWidth <= WIDTH_SINGLE_COL && !shortHeight; - - const noVideoContent = - !playerActive || (section === ROUTE_RECORDINGS && sectionId !== ''); - const shouldDisplayChat = - displayChatPanel && !chatDisabled && chatInputEnabled; - - const extraAppClasses = classNames({ - 'config-loading': configData.loading, - - chat: shouldDisplayChat, - 'no-chat': !shouldDisplayChat, - 'no-video': noVideoContent, - 'chat-hidden': !displayChatPanel && canChat && !chatDisabled, // hide panel - 'chat-disabled': !canChat || chatDisabled, - 'single-col': singleColMode, - 'bg-gray-800': singleColMode && shouldDisplayChat, - 'short-wide': shortHeight && windowWidth > WIDTH_SINGLE_COL, - 'touch-screen': this.hasTouchScreen, - 'touch-keyboard-active': touchKeyboardActive, - }); - - const poster = isPlaying - ? null - : html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `; - - // modal buttons - const externalActionButtons = html`
- ${externalActions && - externalActions.map( - function (action) { - return html`<${ExternalActionButton} - onClick=${this.displayExternalAction} - action=${action} - />`; - }.bind(this) - )} - - - ${federation.enabled && - html`<${FediverseFollowButton} - onClick=${this.displayFediverseFollowModal} - federationInfo=${federation} - serverName=${name} - />`} -
`; - - // modal component - const externalActionModal = - externalActionModalData && - html`<${ExternalActionModal} - action=${externalActionModalData} - onClose=${this.closeExternalActionModal} - />`; - - const fediverseFollowModal = - fediverseModalData && - html` - <${ExternalActionModal} - onClose=${this.closeFediverseFollowModal} - action=${fediverseModalData} - useIframe=${false} - customContent=${html`<${FediverseFollowModal} - name=${name} - logo=${logo} - federationInfo=${federation} - onClose=${this.closeFediverseFollowModal} - />`} - /> - `; - - const chat = this.state.websocket - ? html` - <${Chat} - websocket=${websocket} - username=${username} - chatInputEnabled=${chatInputEnabled && !chatDisabled} - instanceTitle=${name} - accessToken=${this.state.accessToken} - inputMaxBytes=${maxSocketPayloadSize - EST_SOCKET_PAYLOAD_BUFFER || - CHAT_MAX_MESSAGE_LENGTH} - /> - ` - : null; - - const TAB_CONTENT = [ - { - label: 'About', - content: html` -
-
-
- ${tagList && `#${tagList}`} -
-
-
- `, - }, - ]; - - if (federation.enabled) { - TAB_CONTENT.push({ - label: html`Followers - ${federation.followerCount > 10 - ? `${' '}(${federation.followerCount})` - : null}`, - content: html`<${Followers} />`, - }); - } - - return html` -
- - -
-
-

- - - - ${streamOnline && streamTitle ? streamTitle : name} -

-
- <${UsernameForm} - username=${username} - isModerator=${isModerator} - onUsernameChange=${this.handleUsernameChange} - onFocus=${this.handleFormFocus} - onBlur=${this.handleFormBlur} - /> - -
-
-
- -
-
- - ${poster} -
- -
- ${streamStatusMessage} - ${viewerCountMessage} -
-
- -
- ${externalActionButtons && html`${externalActionButtons}`} - -
-
-
- -
- -
- -
-

- ${name} -

-

- ${streamOnline && streamTitle} -

- - -
- <${TabBar} tabs=${TAB_CONTENT} ariaLabel="User Content" /> -
-
-
-
- - - - ${chat} ${externalActionModal} ${fediverseFollowModal} -
- `; - } -} diff --git a/webroot/js/chat/register.js b/webroot/js/chat/register.js deleted file mode 100644 index 5312904..0000000 --- a/webroot/js/chat/register.js +++ /dev/null @@ -1,19 +0,0 @@ -import { URL_CHAT_REGISTRATION } from '../utils/constants.js'; - -export async function registerChat(username) { - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ displayName: username }), - }; - - try { - const response = await fetch(URL_CHAT_REGISTRATION, options); - const result = await response.json(); - return result; - } catch (e) { - console.error(e); - } -} diff --git a/webroot/js/components/chat/chat-input.js b/webroot/js/components/chat/chat-input.js deleted file mode 100644 index 3d3b5dc..0000000 --- a/webroot/js/components/chat/chat-input.js +++ /dev/null @@ -1,399 +0,0 @@ -import { h, Component, createRef } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); - -import { EmojiButton } from '/js/web_modules/@joeattardi/emoji-button.js'; - -import ContentEditable, { replaceCaret } from './content-editable.js'; -import { - generatePlaceholderText, - getCaretPosition, - convertToText, - convertOnPaste, - createEmojiMarkup, - trimNbsp, - emojify, -} from '../../utils/chat.js'; -import { - getLocalStorage, - setLocalStorage, - classNames, -} from '../../utils/helpers.js'; -import { - URL_CUSTOM_EMOJIS, - KEY_CHAT_FIRST_MESSAGE_SENT, - CHAT_CHAR_COUNT_BUFFER, - CHAT_OK_KEYCODES, - CHAT_KEY_MODIFIERS, -} from '../../utils/constants.js'; - -export default class ChatInput extends Component { - constructor(props, context) { - super(props, context); - this.formMessageInput = createRef(); - this.emojiPickerButton = createRef(); - - this.messageCharCount = 0; - - this.prepNewLine = false; - this.modifierKeyPressed = false; // control/meta/shift/alt - - this.state = { - inputHTML: '', - inputCharsLeft: props.inputMaxBytes, - hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT), - emojiPicker: null, - emojiList: null, - emojiNames: null, - }; - - this.handleEmojiButtonClick = this.handleEmojiButtonClick.bind(this); - this.handleEmojiSelected = this.handleEmojiSelected.bind(this); - this.getCustomEmojis = this.getCustomEmojis.bind(this); - - this.handleMessageInputKeydown = this.handleMessageInputKeydown.bind(this); - this.handleMessageInputKeyup = this.handleMessageInputKeyup.bind(this); - this.handleMessageInputBlur = this.handleMessageInputBlur.bind(this); - this.handleSubmitChatButton = this.handleSubmitChatButton.bind(this); - this.handlePaste = this.handlePaste.bind(this); - - this.handleContentEditableChange = - this.handleContentEditableChange.bind(this); - } - - componentDidMount() { - this.getCustomEmojis(); - } - - getCustomEmojis() { - fetch(URL_CUSTOM_EMOJIS) - .then((response) => { - if (!response.ok) { - throw new Error(`Network response was not ok ${response.ok}`); - } - return response.json(); - }) - .then((json) => { - const emojiList = json; - const emojiNames = emojiList.map((emoji) => emoji.name); - const emojiPicker = new EmojiButton({ - zIndex: 100, - theme: 'owncast', // see chat.css - custom: json, - initialCategory: 'custom', - showPreview: false, - autoHide: false, - autoFocusSearch: false, - showAnimation: false, - emojiSize: '24px', - position: 'right-start', - strategy: 'absolute', - }); - emojiPicker.on('emoji', (emoji) => { - this.handleEmojiSelected(emoji); - }); - emojiPicker.on('hidden', () => { - this.formMessageInput.current.focus(); - replaceCaret(this.formMessageInput.current); - }); - this.setState({ emojiNames, emojiList, emojiPicker }); - }) - .catch((error) => { - // this.handleNetworkingError(`Emoji Fetch: ${error}`); - }); - } - - handleEmojiButtonClick() { - const { emojiPicker } = this.state; - if (emojiPicker) { - emojiPicker.togglePicker(this.emojiPickerButton.current); - } - } - - handleEmojiSelected(emoji) { - const { inputHTML, inputCharsLeft } = this.state; - // if we're already at char limit, don't do anything - if (inputCharsLeft < 0) { - return; - } - let content = ''; - if (emoji.url) { - content = createEmojiMarkup(emoji, false); - } else { - content = emoji.emoji; - } - - const position = getCaretPosition(this.formMessageInput.current); - const newHTML = - inputHTML.substring(0, position) + - content + - inputHTML.substring(position); - - const charsLeft = this.calculateCurrentBytesLeft(newHTML); - this.setState({ - inputHTML: newHTML, - inputCharsLeft: charsLeft, - }); - // a hacky way add focus back into input field - setTimeout(() => { - const input = this.formMessageInput.current; - input.focus(); - replaceCaret(input); - }, 100); - } - - // autocomplete text from the given "list". "token" marks the start of word lookup. - autoComplete(token, list) { - const { inputHTML } = this.state; - const position = getCaretPosition(this.formMessageInput.current); - const at = inputHTML.lastIndexOf(token, position - 1); - if (at === -1) { - return false; - } - - let partial = inputHTML.substring(at + 1, position).trim(); - - if (this.partial === undefined) { - this.partial = []; - } - - if (partial === this.suggestion) { - partial = this.partial[token]; - } else { - this.partial[token] = partial; - } - - const possibilities = list.filter(function (item) { - return item.toLowerCase().startsWith(partial.toLowerCase()); - }); - - if (this.completionIndex === undefined) { - this.completionIndex = []; - } - - if ( - this.completionIndex[token] === undefined || - ++this.completionIndex[token] >= possibilities.length - ) { - this.completionIndex[token] = 0; - } - - if (possibilities.length > 0) { - this.suggestion = possibilities[this.completionIndex[token]]; - - const newHTML = - inputHTML.substring(0, at + 1) + - this.suggestion + - ' ' + - inputHTML.substring(position); - - this.setState({ - inputHTML: newHTML, - inputCharsLeft: this.calculateCurrentBytesLeft(newHTML), - }); - } - - return true; - } - - // replace :emoji: with the emoji - injectEmoji() { - const { inputHTML, emojiList } = this.state; - const textValue = convertToText(inputHTML); - const processedHTML = emojify(inputHTML, emojiList); - - if (textValue != convertToText(processedHTML)) { - this.setState({ - inputHTML: processedHTML, - }); - return true; - } - return false; - } - - handleMessageInputKeydown(event) { - const key = event && event.key; - - if (key === 'Enter') { - if (!this.prepNewLine) { - this.sendMessage(); - event.preventDefault(); - this.prepNewLine = false; - return; - } - } - // allow key presses such as command/shift/meta, etc even when message length is full later. - if (CHAT_KEY_MODIFIERS.includes(key)) { - this.modifierKeyPressed = true; - } - if (key === 'Control' || key === 'Shift') { - this.prepNewLine = true; - } - if (key === 'Tab') { - const { chatUserNames } = this.props; - const { emojiNames } = this.state; - if (this.autoComplete('@', chatUserNames)) { - event.preventDefault(); - } - if (this.autoComplete(':', emojiNames)) { - event.preventDefault(); - } - } - - // if new input pushes the potential chars over, don't do anything - const formField = this.formMessageInput.current; - const tempCharsLeft = this.calculateCurrentBytesLeft(formField.innerHTML); - if (tempCharsLeft <= 0 && !CHAT_OK_KEYCODES.includes(key)) { - if (!this.modifierKeyPressed) { - event.preventDefault(); // prevent typing more - } - return; - } - } - - handleMessageInputKeyup(event) { - const { key } = event; - if (key === 'Control' || key === 'Shift') { - this.prepNewLine = false; - } - if (CHAT_KEY_MODIFIERS.includes(key)) { - this.modifierKeyPressed = false; - } - - if (key === ':' || key === ';') { - this.injectEmoji(); - } - } - - handleMessageInputBlur() { - this.prepNewLine = false; - this.modifierKeyPressed = false; - } - - handlePaste(event) { - // don't allow paste if too much text already - if (this.state.inputCharsLeft < 0) { - event.preventDefault(); - return; - } - convertOnPaste(event, this.state.emojiList); - this.handleMessageInputKeydown(event); - } - - handleSubmitChatButton(event) { - event.preventDefault(); - this.sendMessage(); - } - - sendMessage() { - const { handleSendMessage, inputMaxBytes } = this.props; - const { hasSentFirstChatMessage, inputHTML, inputCharsLeft } = this.state; - if (inputCharsLeft < 0) { - return; - } - const message = convertToText(inputHTML); - const newStates = { - inputHTML: '', - inputCharsLeft: inputMaxBytes, - }; - - handleSendMessage(message); - - if (!hasSentFirstChatMessage) { - newStates.hasSentFirstChatMessage = true; - setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true); - } - - // clear things out. - this.setState(newStates); - } - - handleContentEditableChange(event) { - const value = event.target.value; - this.setState({ - inputHTML: value, - inputCharsLeft: this.calculateCurrentBytesLeft(value), - }); - } - - calculateCurrentBytesLeft(inputContent) { - const { inputMaxBytes } = this.props; - const curBytes = new Blob([trimNbsp(inputContent)]).size; - return inputMaxBytes - curBytes; - } - - render(props, state) { - const { hasSentFirstChatMessage, inputCharsLeft, inputHTML, emojiPicker } = - state; - const { inputEnabled, inputMaxBytes } = props; - const emojiButtonStyle = { - display: emojiPicker && inputCharsLeft > 0 ? 'block' : 'none', - }; - const extraClasses = classNames({ - 'display-count': inputCharsLeft <= CHAT_CHAR_COUNT_BUFFER, - }); - const placeholderText = generatePlaceholderText( - inputEnabled, - hasSentFirstChatMessage - ); - return html` -
-
- <${ContentEditable} - id="message-input" - class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none" - placeholderText=${placeholderText} - innerRef=${this.formMessageInput} - html=${inputHTML} - disabled=${!inputEnabled} - onChange=${this.handleContentEditableChange} - onKeyDown=${this.handleMessageInputKeydown} - onKeyUp=${this.handleMessageInputKeyup} - onBlur=${this.handleMessageInputBlur} - onPaste=${this.handlePaste} - /> -
-
- - - - - - - ${inputCharsLeft} bytes -
-
- `; - } -} diff --git a/webroot/js/components/chat/chat-message-view.js b/webroot/js/components/chat/chat-message-view.js deleted file mode 100644 index 1ff813c..0000000 --- a/webroot/js/components/chat/chat-message-view.js +++ /dev/null @@ -1,212 +0,0 @@ -import { h, Component } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -import Mark from '/js/web_modules/markjs/dist/mark.es6.min.js'; -const html = htm.bind(h); - -import { - messageBubbleColorForHue, - textColorForHue, -} from '../../utils/user-colors.js'; -import { convertToText, checkIsModerator } from '../../utils/chat.js'; -import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; -import { getDiffInDaysFromNow } from '../../utils/helpers.js'; -import ModeratorActions from './moderator-actions.js'; - -export default class ChatMessageView extends Component { - constructor(props) { - super(props); - this.state = { - formattedMessage: '', - moderatorMenuOpen: false, - }; - } - - shouldComponentUpdate(nextProps, nextState) { - const { formattedMessage } = this.state; - const { formattedMessage: nextFormattedMessage } = nextState; - - return ( - formattedMessage !== nextFormattedMessage || - (!this.props.isModerator && nextProps.isModerator) - ); - } - - async componentDidMount() { - const { message, username } = this.props; - const { body } = message; - - if (message && username) { - const formattedMessage = await formatMessageText(body, username); - this.setState({ - formattedMessage, - }); - } - } - render() { - const { message, isModerator, accessToken } = this.props; - const { user, timestamp } = message; - - // User is required for this component to render. - if (!user) { - return null; - } - - const { displayName, displayColor, createdAt } = user; - const isAuthorModerator = checkIsModerator(message); - - const isMessageModeratable = - isModerator && message.type === SOCKET_MESSAGE_TYPES.CHAT; - - const { formattedMessage } = this.state; - if (!formattedMessage) { - return null; - } - const formattedTimestamp = `Sent at ${formatTimestamp(timestamp)}`; - const userMetadata = createdAt - ? `${displayName} first joined ${formatTimestamp(createdAt)}` - : null; - - const isSystemMessage = message.type === SOCKET_MESSAGE_TYPES.SYSTEM; - - const authorTextColor = isSystemMessage - ? { color: '#fff' } - : { color: textColorForHue(displayColor) }; - const backgroundStyle = isSystemMessage - ? { backgroundColor: '#667eea' } - : { backgroundColor: messageBubbleColorForHue(displayColor) }; - const messageClassString = isSystemMessage - ? 'message flex flex-row items-start p-4 m-2 rounded-lg shadow-l border-solid border-indigo-700 border-2 border-opacity-60 text-l' - : `message relative flex flex-row items-start p-3 m-3 rounded-lg shadow-s text-sm ${ - isMessageModeratable ? 'moderatable' : '' - }`; - - const messageAuthorFlair = isAuthorModerator - ? html`` - : null; - - return html` -
-
-
- ${messageAuthorFlair} ${displayName} -
- ${isMessageModeratable && - html`<${ModeratorActions} - message=${message} - accessToken=${accessToken} - />`} -
-
-
- `; - } -} - -export async function formatMessageText(message, username) { - let formattedText = getMessageWithEmbeds(message); - formattedText = convertToMarkup(formattedText); - return await highlightUsername(formattedText, username); -} - -function highlightUsername(message, username) { - // https://github.com/julmot/mark.js/issues/115 - const node = document.createElement('span'); - node.innerHTML = message; - return new Promise((res) => { - new Mark(node).mark(username, { - element: 'span', - className: 'highlighted px-1 rounded font-bold bg-orange-500', - separateWordSearch: false, - accuracy: { - value: 'exactly', - limiters: [',', '.', "'", '?', '@'], - }, - done() { - res(node.innerHTML); - }, - }); - }); -} - -function getMessageWithEmbeds(message) { - var embedText = ''; - // Make a temporary element so we can actually parse the html and pull anchor tags from it. - // This is a better approach than regex. - var container = document.createElement('p'); - container.innerHTML = message; - - var anchors = container.getElementsByTagName('a'); - for (var i = 0; i < anchors.length; i++) { - const url = anchors[i].href; - if (url.indexOf('instagram.com/p/') > -1) { - embedText += getInstagramEmbedFromURL(url); - } - } - - // If this message only consists of a single embeddable link - // then only return the embed and strip the link url from the text. - if ( - embedText !== '' && - anchors.length == 1 && - isMessageJustAnchor(message, anchors[0]) - ) { - return embedText; - } - return message + embedText; -} - -function getInstagramEmbedFromURL(url) { - const urlObject = new URL(url.replace(/\/$/, '')); - urlObject.pathname += '/embed'; - return ``; -} - -function isMessageJustAnchor(message, anchor) { - return stripTags(message) === stripTags(anchor.innerHTML); -} - -function formatTimestamp(sentAt) { - sentAt = new Date(sentAt); - if (isNaN(sentAt)) { - return ''; - } - - let diffInDays = getDiffInDaysFromNow(sentAt); - if (diffInDays >= 1) { - return ( - `at ${sentAt.toLocaleDateString('en-US', { - dateStyle: 'medium', - })} at ` + sentAt.toLocaleTimeString() - ); - } - - return `${sentAt.toLocaleTimeString()}`; -} - -/* - You would call this when receiving a plain text - value back from an API, and before inserting the - text into the `contenteditable` area on a page. -*/ -function convertToMarkup(str = '') { - return convertToText(str).replace(/\n/g, '

'); -} - -function stripTags(str) { - return str.replace(/<\/?[^>]+(>|$)/g, ''); -} diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js deleted file mode 100644 index 244ed5e..0000000 --- a/webroot/js/components/chat/chat.js +++ /dev/null @@ -1,508 +0,0 @@ -import { h, Component, createRef } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); - -import Message from './message.js'; -import ChatInput from './chat-input.js'; -import { CALLBACKS, SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; -import { jumpToBottom, debounce } from '../../utils/helpers.js'; -import { - extraUserNamesFromMessageHistory, - checkIsModerator, -} from '../../utils/chat.js'; -import { - URL_CHAT_HISTORY, - MESSAGE_JUMPTOBOTTOM_BUFFER, -} from '../../utils/constants.js'; - -const MAX_RENDER_BACKLOG = 300; - -// Add message types that should be displayed in chat to this array. -const renderableChatStyleMessages = [ - SOCKET_MESSAGE_TYPES.NAME_CHANGE, - SOCKET_MESSAGE_TYPES.CONNECTED_USER_INFO, - SOCKET_MESSAGE_TYPES.USER_JOINED, - SOCKET_MESSAGE_TYPES.CHAT_ACTION, - SOCKET_MESSAGE_TYPES.SYSTEM, - SOCKET_MESSAGE_TYPES.CHAT, - SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_FOLLOW, - SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_LIKE, - SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_REPOST, -]; -export default class Chat extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - chatUserNames: [], - // Ordered array of messages sorted by timestamp. - sortedMessages: [], - - newMessagesReceived: false, - webSocketConnected: true, - isModerator: false, - }; - - this.scrollableMessagesContainer = createRef(); - - this.websocket = null; - this.receivedFirstMessages = false; - this.receivedMessageUpdate = false; - this.hasFetchedHistory = false; - - // Unordered dictionary of messages keyed by ID. - this.messages = {}; - - this.windowBlurred = false; - this.numMessagesSinceBlur = 0; - - this.getChatHistory = this.getChatHistory.bind(this); - this.handleNetworkingError = this.handleNetworkingError.bind(this); - this.handleWindowBlur = this.handleWindowBlur.bind(this); - this.handleWindowFocus = this.handleWindowFocus.bind(this); - this.handleWindowResize = debounce(this.handleWindowResize.bind(this), 500); - this.messageListCallback = this.messageListCallback.bind(this); - this.receivedWebsocketMessage = this.receivedWebsocketMessage.bind(this); - this.scrollToBottom = this.scrollToBottom.bind(this); - this.submitChat = this.submitChat.bind(this); - this.websocketConnected = this.websocketConnected.bind(this); - this.websocketDisconnected = this.websocketDisconnected.bind(this); - } - - componentDidMount() { - this.setupWebSocketCallbacks(); - - window.addEventListener('resize', this.handleWindowResize); - - if (!this.props.readonly) { - window.addEventListener('blur', this.handleWindowBlur); - window.addEventListener('focus', this.handleWindowFocus); - } - - this.messageListObserver = new MutationObserver(this.messageListCallback); - this.messageListObserver.observe(this.scrollableMessagesContainer.current, { - childList: true, - }); - } - - shouldComponentUpdate(nextProps, nextState) { - const { username, chatInputEnabled } = this.props; - const { username: nextUserName, chatInputEnabled: nextChatEnabled } = - nextProps; - - const { - webSocketConnected, - chatUserNames, - newMessagesReceived, - sortedMessages, - } = this.state; - - const { - webSocketConnected: nextSocket, - chatUserNames: nextUserNames, - newMessagesReceived: nextMessagesReceived, - } = nextState; - - // If there are an updated number of sorted message then a render pass - // needs to take place to render these new messages. - if ( - Object.keys(sortedMessages).length !== - Object.keys(nextState.sortedMessages).length - ) { - return true; - } - - if (newMessagesReceived) { - return true; - } - - return ( - username !== nextUserName || - chatInputEnabled !== nextChatEnabled || - webSocketConnected !== nextSocket || - chatUserNames.length !== nextUserNames.length || - newMessagesReceived !== nextMessagesReceived - ); - } - - componentDidUpdate(prevProps, prevState) { - const { accessToken } = this.props; - - // Fetch chat history - if (!this.hasFetchedHistory && accessToken) { - this.hasFetchedHistory = true; - this.getChatHistory(accessToken); - } - } - - componentWillUnmount() { - window.removeEventListener('resize', this.handleWindowResize); - if (!this.props.readonly) { - window.removeEventListener('blur', this.handleWindowBlur); - window.removeEventListener('focus', this.handleWindowFocus); - } - this.messageListObserver.disconnect(); - } - - setupWebSocketCallbacks() { - this.websocket = this.props.websocket; - if (this.websocket) { - this.websocket.addListener( - CALLBACKS.RAW_WEBSOCKET_MESSAGE_RECEIVED, - this.receivedWebsocketMessage - ); - this.websocket.addListener( - CALLBACKS.WEBSOCKET_CONNECTED, - this.websocketConnected - ); - this.websocket.addListener( - CALLBACKS.WEBSOCKET_DISCONNECTED, - this.websocketDisconnected - ); - } - } - - // fetch chat history - async getChatHistory(accessToken) { - const { username } = this.props; - try { - const response = await fetch( - URL_CHAT_HISTORY + `?accessToken=${accessToken}` - ); - const data = await response.json(); - - // Backlog of usernames from history - const allChatUserNames = extraUserNamesFromMessageHistory(data); - const chatUserNames = allChatUserNames.filter((name) => name != username); - - this.addNewRenderableMessages(data); - - this.setState((previousState) => { - return { - ...previousState, - chatUserNames, - }; - }); - } catch (error) { - this.handleNetworkingError(`Fetch getChatHistory: ${error}`); - } - - this.scrollToBottom(); - } - - receivedWebsocketMessage(message) { - this.handleMessage(message); - } - - handleNetworkingError(error) { - // todo: something more useful - console.error('chat error', error); - } - - // Give a list of message IDs and the visibility state they should change to. - updateMessagesVisibility(idsToUpdate, visible) { - let messageList = { ...this.messages }; - - // Iterate through each ID and mark the associated ID in our messages - // dictionary with the new visibility. - for (const id of idsToUpdate) { - const message = messageList[id]; - if (message) { - message.visible = visible; - messageList[id] = message; - } - } - - const updatedMessagesList = { - ...this.messages, - ...messageList, - }; - - this.messages = updatedMessagesList; - - this.resortAndRenderMessages(); - } - - handleChangeModeratorStatus(isModerator) { - if (isModerator !== this.state.isModerator) { - this.setState((previousState) => { - return { ...previousState, isModerator: isModerator }; - }); - } - } - - handleWindowFocusNotificationCount(readonly, messageType) { - // if window is blurred and we get a new message, add 1 to title - if ( - !readonly && - messageType === SOCKET_MESSAGE_TYPES.CHAT && - this.windowBlurred - ) { - this.numMessagesSinceBlur += 1; - } - } - - addNewRenderableMessages(messagesArray) { - // Convert the array of chat history messages into an object - // to be merged with the existing chat messages. - const newMessages = messagesArray.reduce( - (o, message) => ({ ...o, [message.id]: message }), - {} - ); - - // Keep our unsorted collection of messages keyed by ID. - const updatedMessagesList = { - ...newMessages, - ...this.messages, - }; - this.messages = updatedMessagesList; - - this.resortAndRenderMessages(); - } - - resortAndRenderMessages() { - // Convert the unordered dictionary of messages to an ordered array. - // NOTE: This sorts the entire collection of messages on every new message - // because the order a message comes in cannot be trusted that it's the order - // it was sent, you need to sort by timestamp. I don't know if there - // is a performance problem waiting to occur here for larger chat feeds. - var sortedMessages = Object.values(this.messages) - // Filter out messages set to not be visible - .filter((message) => message.visible !== false) - .sort((a, b) => { - return Date.parse(a.timestamp) - Date.parse(b.timestamp); - }); - - // Cap this list to 300 items to improve browser performance. - if (sortedMessages.length >= MAX_RENDER_BACKLOG) { - sortedMessages = sortedMessages.slice( - sortedMessages.length - MAX_RENDER_BACKLOG - ); - } - - this.setState((previousState) => { - return { - ...previousState, - newMessagesReceived: true, - sortedMessages, - }; - }); - } - - // handle any incoming message - handleMessage(message) { - const { type: messageType } = message; - const { readonly, username } = this.props; - - // Allow non-user chat messages to be visible by default. - const messageVisible = - message.visible || messageType !== SOCKET_MESSAGE_TYPES.CHAT; - - // Show moderator status - if (messageType === SOCKET_MESSAGE_TYPES.CONNECTED_USER_INFO) { - const modStatusUpdate = checkIsModerator(message); - this.handleChangeModeratorStatus(modStatusUpdate); - } - - // Change the visibility of messages by ID. - if (messageType === SOCKET_MESSAGE_TYPES.VISIBILITY_UPDATE) { - const idsToUpdate = message.ids; - const visible = message.visible; - this.updateMessagesVisibility(idsToUpdate, visible); - } else if ( - renderableChatStyleMessages.includes(messageType) && - messageVisible - ) { - // Add new message to the chat feed. - this.addNewRenderableMessages([message]); - - // Update the usernames list, filtering out our own name. - const updatedAllChatUserNames = this.updateAuthorList(message); - if (updatedAllChatUserNames.length) { - const updatedChatUserNames = updatedAllChatUserNames.filter( - (name) => name != username - ); - this.setState((previousState) => { - return { - ...previousState, - chatUserNames: [...updatedChatUserNames], - }; - }); - } - } - - // Update the window title if needed. - this.handleWindowFocusNotificationCount(readonly, messageType); - } - - websocketConnected() { - this.setState((previousState) => { - return { - ...previousState, - webSocketConnected: true, - }; - }); - } - - websocketDisconnected() { - this.setState((previousState) => { - return { - ...previousState, - webSocketConnected: false, - }; - }); - } - - submitChat(content) { - if (!content) { - return; - } - const message = { - body: content, - type: SOCKET_MESSAGE_TYPES.CHAT, - }; - this.websocket.send(message); - } - - updateAuthorList(message) { - const { type } = message; - let nameList = this.state.chatUserNames; - - if ( - type === SOCKET_MESSAGE_TYPES.CHAT && - !nameList.includes(message.user.displayName) - ) { - nameList.push(message.user.displayName); - return nameList; - } else if (type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { - const { oldName, user } = message; - const oldNameIndex = nameList.indexOf(oldName); - nameList.splice(oldNameIndex, 1, user.displayName); - return nameList; - } - - return []; - } - - scrollToBottom() { - jumpToBottom(this.scrollableMessagesContainer.current); - } - - checkShouldScroll() { - const { scrollTop, scrollHeight, clientHeight } = - this.scrollableMessagesContainer.current; - const fullyScrolled = scrollHeight - clientHeight; - const shouldScroll = - scrollHeight >= clientHeight && - fullyScrolled - scrollTop < MESSAGE_JUMPTOBOTTOM_BUFFER; - - return shouldScroll; - } - - handleWindowResize() { - this.scrollToBottom(); - } - - handleWindowBlur() { - this.windowBlurred = true; - } - - handleWindowFocus() { - this.windowBlurred = false; - this.numMessagesSinceBlur = 0; - window.document.title = this.props.instanceTitle; - } - - // if the messages list grows in number of child message nodes due to new messages received, scroll to bottom. - messageListCallback(mutations) { - const numMutations = mutations.length; - - if (numMutations) { - const item = mutations[numMutations - 1]; - if (item.type === 'childList' && item.addedNodes.length) { - if (this.state.newMessagesReceived) { - if (!this.receivedFirstMessages) { - this.scrollToBottom(); - this.receivedFirstMessages = true; - } else if (this.checkShouldScroll()) { - this.scrollToBottom(); - } - - this.setState((previousState) => { - return { - ...previousState, - newMessagesReceived: false, - }; - }); - } - } - // update document title if window blurred - if ( - this.numMessagesSinceBlur && - !this.props.readonly && - this.windowBlurred - ) { - this.updateDocumentTitle(); - } - } - } - - updateDocumentTitle() { - const num = - this.numMessagesSinceBlur > 10 ? '10+' : this.numMessagesSinceBlur; - window.document.title = `${num} 💬 :: ${this.props.instanceTitle}`; - } - - render(props, state) { - const { username, readonly, chatInputEnabled, inputMaxBytes, accessToken } = - props; - const { sortedMessages, chatUserNames, webSocketConnected, isModerator } = - state; - - const messageList = sortedMessages.map( - (message) => - html`<${Message} - message=${message} - username=${username} - key=${message.id} - isModerator=${isModerator} - accessToken=${accessToken} - />` - ); - - if (readonly) { - return html` -
- ${messageList} -
- `; - } - - return html` -
-
-
- ${messageList} -
- <${ChatInput} - chatUserNames=${chatUserNames} - inputEnabled=${webSocketConnected && chatInputEnabled} - handleSendMessage=${this.submitChat} - inputMaxBytes=${inputMaxBytes} - /> -
-
- `; - } -} diff --git a/webroot/js/components/chat/content-editable.js b/webroot/js/components/chat/content-editable.js deleted file mode 100644 index fa81bea..0000000 --- a/webroot/js/components/chat/content-editable.js +++ /dev/null @@ -1,129 +0,0 @@ -/* -Since we can't really import react-contenteditable here, I'm borrowing code for this component from here: -github.com/lovasoa/react-contenteditable/ - -and here: -https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103 - -*/ -import { h, Component, createRef } from '/js/web_modules/preact.js'; - -export function replaceCaret(el) { - // Place the caret at the end of the element - const target = document.createTextNode(''); - el.appendChild(target); - // do not move caret if element was not focused - const isTargetFocused = document.activeElement === el; - if (target !== null && target.nodeValue !== null && isTargetFocused) { - var sel = window.getSelection(); - if (sel !== null) { - var range = document.createRange(); - range.setStart(target, target.nodeValue.length); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - } - if (el) el.focus(); - } -} - -function normalizeHtml(str) { - return str && str.replace(/ |\u202F|\u00A0/g, ' '); -} - - - -export default class ContentEditable extends Component { - constructor(props) { - super(props); - - this.el = createRef(); - - this.lastHtml = ''; - - this.emitChange = this.emitChange.bind(this); - this.getDOMElement = this.getDOMElement.bind(this); - } - - shouldComponentUpdate(nextProps) { - const { props } = this; - const el = this.getDOMElement(); - - // We need not rerender if the change of props simply reflects the user's edits. - // Rerendering in this case would make the cursor/caret jump - - // Rerender if there is no element yet... (somehow?) - if (!el) return true; - - // ...or if html really changed... (programmatically, not by user edit) - if ( - normalizeHtml(nextProps.html) !== normalizeHtml(el.innerHTML) - ) { - return true; - } - - // Handle additional properties - return props.disabled !== nextProps.disabled || - props.tagName !== nextProps.tagName || - props.className !== nextProps.className || - props.innerRef !== nextProps.innerRef; - } - - componentDidUpdate() { - const el = this.getDOMElement(); - if (!el) return; - - // Perhaps React (whose VDOM gets outdated because we often prevent - // rerendering) did not update the DOM. So we update it manually now. - if (this.props.html !== el.innerHTML) { - el.innerHTML = this.props.html; - } - this.lastHtml = this.props.html; - replaceCaret(el); - } - - getDOMElement() { - return (this.props.innerRef && typeof this.props.innerRef !== 'function' ? this.props.innerRef : this.el).current; - } - - - emitChange(originalEvt) { - const el = this.getDOMElement(); - if (!el) return; - - const html = el.innerHTML; - if (this.props.onChange && html !== this.lastHtml) { - // Clone event with Object.assign to avoid - // "Cannot assign to read only property 'target' of object" - const evt = Object.assign({}, originalEvt, { - target: { - value: html - } - }); - this.props.onChange(evt); - } - this.lastHtml = html; - } - - render(props) { - const { html, innerRef } = props; - return h( - 'div', - { - ...props, - ref: typeof innerRef === 'function' ? (current) => { - innerRef(current) - this.el.current = current - } : innerRef || this.el, - onInput: this.emitChange, - onFocus: this.props.onFocus || this.emitChange, - onBlur: this.props.onBlur || this.emitChange, - onKeyup: this.props.onKeyUp || this.emitChange, - onKeydown: this.props.onKeyDown || this.emitChange, - contentEditable: !this.props.disabled, - dangerouslySetInnerHTML: { __html: html }, - }, - this.props.children, - ); - } -} diff --git a/webroot/js/components/chat/message.js b/webroot/js/components/chat/message.js deleted file mode 100644 index aa1d11c..0000000 --- a/webroot/js/components/chat/message.js +++ /dev/null @@ -1,140 +0,0 @@ -import { h } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); - -import ChatMessageView from './chat-message-view.js'; - -import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; -import { checkIsModerator } from '../../utils/chat.js'; - -function SystemMessage(props) { - const { contents } = props; - return html` -
-
-
- ${contents} -
-
-
- `; -} - -function SingleFederatedUser(props) { - const { message } = props; - const { type, body, title, image, link } = message; - - let icon = null; - switch (type) { - case SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_FOLLOW: - icon = '/img/follow.svg'; - break; - case SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_LIKE: - icon = '/img/like.svg'; - break; - case SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_REPOST: - icon = '/img/repost.svg'; - break; - default: - break; - } - - return html` - -
-
- - -
-
-
- ${title} -
-

-
-
-
- `; -} - -export default function Message(props) { - const { message } = props; - const { type, oldName, user, body } = message; - if ( - type === SOCKET_MESSAGE_TYPES.CHAT || - type === SOCKET_MESSAGE_TYPES.SYSTEM - ) { - return html`<${ChatMessageView} ...${props} />`; - } else if (type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) { - // User changed their name - const { displayName } = user; - const contents = html` -
- ${oldName} is now known as ${' '} - ${displayName}. -
- `; - return html`<${SystemMessage} contents=${contents} />`; - } else if (type === SOCKET_MESSAGE_TYPES.USER_JOINED) { - const { displayName } = user; - const isAuthorModerator = checkIsModerator(message); - const messageAuthorFlair = isAuthorModerator - ? html`` - : null; - const contents = html`
- ${messageAuthorFlair}${displayName} - ${' '}joined the chat. -
`; - return html`<${SystemMessage} contents=${contents} />`; - } else if (type === SOCKET_MESSAGE_TYPES.CHAT_ACTION) { - const contents = html``; - return html`<${SystemMessage} contents=${contents} />`; - } else if (type === SOCKET_MESSAGE_TYPES.CONNECTED_USER_INFO) { - // moderator message - const isModerator = checkIsModerator(message); - if (isModerator) { - const contents = html`
- You are now a - moderator. -
`; - return html`<${SystemMessage} contents=${contents} />`; - } - } else if ( - type === SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_FOLLOW || - SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_LIKE || - SOCKET_MESSAGE_TYPES.FEDIVERSE_ENGAGEMENT_REPOST - ) { - return html` <${SingleFederatedUser} message=${message} /> `; - } else { - console.log('Unknown message type:', type); - } -} diff --git a/webroot/js/components/chat/moderator-actions.js b/webroot/js/components/chat/moderator-actions.js deleted file mode 100644 index d90939e..0000000 --- a/webroot/js/components/chat/moderator-actions.js +++ /dev/null @@ -1,298 +0,0 @@ -import { h, Component, createRef } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -import { textColorForHue } from '../../utils/user-colors.js'; -import { URL_BAN_USER, URL_HIDE_MESSAGE } from '../../utils/constants.js'; - -const html = htm.bind(h); - -const HIDE_MESSAGE_ICON = `/img/hide-message-grey.svg`; -const HIDE_MESSAGE_ICON_HOVER = '/img/hide-message.svg'; -const BAN_USER_ICON = '/img/ban-user-grey.svg'; -const BAN_USER_ICON_HOVER = '/img/ban-user.svg'; - -export default class ModeratorActions extends Component { - constructor(props) { - super(props); - this.state = { - isMenuOpen: false, - }; - this.handleOpenMenu = this.handleOpenMenu.bind(this); - this.handleCloseMenu = this.handleCloseMenu.bind(this); - } - - handleOpenMenu() { - this.setState({ - isMenuOpen: true, - }); - } - - handleCloseMenu() { - this.setState({ - isMenuOpen: false, - }); - } - - render() { - const { isMenuOpen } = this.state; - const { message, accessToken } = this.props; - const { id } = message; - const { user } = message; - - return html` -
- - - ${isMenuOpen && - html`<${ModeratorMenu} - message=${message} - onDismiss=${this.handleCloseMenu} - accessToken=${accessToken} - id=${id} - userId=${user.id} - />`} -
- `; - } -} - -class ModeratorMenu extends Component { - constructor(props) { - super(props); - this.menuNode = createRef(); - - this.state = { - displayMoreInfo: false, - }; - this.handleClickOutside = this.handleClickOutside.bind(this); - this.handleToggleMoreInfo = this.handleToggleMoreInfo.bind(this); - this.handleBanUser = this.handleBanUser.bind(this); - this.handleHideMessage = this.handleHideMessage.bind(this); - } - - componentDidMount() { - document.addEventListener('mousedown', this.handleClickOutside, false); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickOutside, false); - } - - handleClickOutside = (e) => { - if ( - this.menuNode && - !this.menuNode.current.contains(e.target) && - this.props.onDismiss - ) { - this.props.onDismiss(); - } - }; - - handleToggleMoreInfo() { - this.setState({ - displayMoreInfo: !this.state.displayMoreInfo, - }); - } - - async handleHideMessage() { - if (!confirm('Are you sure you want to remove this message from chat?')) { - this.props.onDismiss(); - return; - } - - const { accessToken, id } = this.props; - const url = new URL(location.origin + URL_HIDE_MESSAGE); - url.searchParams.append('accessToken', accessToken); - const hideMessageUrl = url.toString(); - - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ idArray: [id] }), - }; - - try { - await fetch(hideMessageUrl, options); - } catch (e) { - console.error(e); - } - - this.props.onDismiss(); - } - - async handleBanUser() { - if (!confirm('Are you sure you want to remove this user from chat?')) { - this.props.onDismiss(); - return; - } - - const { accessToken, userId } = this.props; - const url = new URL(location.origin + URL_BAN_USER); - url.searchParams.append('accessToken', accessToken); - const hideMessageUrl = url.toString(); - - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ userId: userId }), - }; - - try { - await fetch(hideMessageUrl, options); - } catch (e) { - console.error(e); - } - - this.props.onDismiss(); - } - - render() { - const { message } = this.props; - const { displayMoreInfo } = this.state; - return html` - - `; - } -} - -// 3 dots button -function ModeratorMenuItem({ icon, hoverIcon, label, onClick }) { - return html` - - `; -} - -// more details panel that display message, prev usernames, actions -function ModeratorMoreInfoContainer({ - message, - handleHideMessage, - handleBanUser, -}) { - const { user, timestamp, body } = message; - const { displayName, createdAt, previousNames, displayColor } = user; - const isAuthorModerator = user.scopes && user.scopes.includes('MODERATOR'); - - const authorTextColor = { color: textColorForHue(displayColor) }; - const createDate = new Date(createdAt); - const sentDate = new Date(timestamp); - return html` -
-
-

- Sent at ${sentDate.toLocaleTimeString()} -

-
-
-
-

Sent by:

-

- ${displayName} -

- -

- First joined: ${createDate.toLocaleString()} -

- - ${previousNames.length > 1 && - html` -

- Previously known as: ${' '} - ${previousNames.join(', ')} -

- `} -
-
- <${handleHideMessage && ModeratorMenuItem} - icon=${HIDE_MESSAGE_ICON} - hoverIcon=${HIDE_MESSAGE_ICON_HOVER} - label="Hide message" - onClick="${handleHideMessage}" - /> - <${handleBanUser && ModeratorMenuItem} - icon=${BAN_USER_ICON} - hoverIcon=${BAN_USER_ICON_HOVER} - label="Ban user" - onClick="${handleBanUser}" - /> -
-
- `; -} diff --git a/webroot/js/components/chat/username.js b/webroot/js/components/chat/username.js deleted file mode 100644 index fb4be11..0000000 --- a/webroot/js/components/chat/username.js +++ /dev/null @@ -1,158 +0,0 @@ -import { h, Component, createRef } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -const html = htm.bind(h); - -import { setLocalStorage } from '../../utils/helpers.js'; -import { - KEY_USERNAME, - KEY_CUSTOM_USERNAME_SET, -} from '../../utils/constants.js'; - -export default class UsernameForm extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - displayForm: false, - isFocused: false, - }; - - this.textInput = createRef(); - - this.handleKeydown = this.handleKeydown.bind(this); - this.handleDisplayForm = this.handleDisplayForm.bind(this); - this.handleHideForm = this.handleHideForm.bind(this); - this.handleUpdateUsername = this.handleUpdateUsername.bind(this); - this.handleFocus = this.handleFocus.bind(this); - this.handleBlur = this.handleBlur.bind(this); - } - - handleDisplayForm() { - const { displayForm: curDisplay } = this.state; - this.setState({ - displayForm: !curDisplay, - }); - } - - handleHideForm() { - this.setState({ - displayForm: false, - }); - } - - handleKeydown(event) { - if (event.keyCode === 13) { - // enter - this.handleUpdateUsername(); - } else if (event.keyCode === 27) { - // esc - this.handleHideForm(); - } - } - - handleUpdateUsername() { - const { username: curName, onUsernameChange } = this.props; - let newName = this.textInput.current.value; - newName = newName.trim(); - if (newName !== '' && newName !== curName) { - setLocalStorage(KEY_USERNAME, newName); - // So we know that the user has set a custom name - setLocalStorage(KEY_CUSTOM_USERNAME_SET, true); - if (onUsernameChange) { - onUsernameChange(newName); - } - this.handleHideForm(); - } - } - - handleFocus() { - const { onFocus } = this.props; - if (onFocus) { - onFocus(); - } - } - - handleBlur() { - const { onBlur } = this.props; - if (onBlur) { - onBlur(); - } - } - - render(props, state) { - const { username, isModerator } = props; - const { displayForm } = state; - - const styles = { - info: { - display: displayForm ? 'none' : 'flex', - }, - form: { - display: displayForm ? 'flex' : 'none', - }, - }; - - const moderatorFlag = html` - - `; - const userIcon = html` - - `; - - return html` -
-
- ${isModerator ? moderatorFlag : userIcon}${username} -
- -
- - - - -
-
- `; - } -} diff --git a/webroot/js/components/external-action-modal.js b/webroot/js/components/external-action-modal.js deleted file mode 100644 index 111ebd8..0000000 --- a/webroot/js/components/external-action-modal.js +++ /dev/null @@ -1,128 +0,0 @@ -import { h, Component } from '/js/web_modules/preact.js'; -import htm from '/js/web_modules/htm.js'; -import MicroModal from '/js/web_modules/micromodal/dist/micromodal.min.js'; - -const html = htm.bind(h); - -export default class ExternalActionModal extends Component { - constructor(props) { - super(props); - this.state = { - iframeLoaded: false, - }; - - this.setIframeLoaded = this.setIframeLoaded.bind(this); - } - componentDidMount() { - // initalize and display Micromodal on mount - try { - MicroModal.init({ - awaitCloseAnimation: false, - awaitOpenAnimation: true, // if using css animations to open the modal. This allows it to wait for the animation to finish before focusing on an element inside the modal. - }); - MicroModal.show('external-actions-modal', { - onClose: this.props.onClose, - }); - } catch (e) { - console.error('modal error: ', e); - } - } - - setIframeLoaded() { - this.setState({ - iframeLoaded: true, - }); - } - - render() { - const { action, useIframe = true, customContent = null } = this.props; - const { url, title, description } = action; - const { iframeLoaded } = this.state; - const iframeStyle = iframeLoaded - ? null - : { backgroundImage: 'url(/img/loading.gif)' }; - - return html` -