diff --git a/.prettierrc.js b/.prettierrc.js
index ae7b27fbfa2fd96a0c3b4dd4fe2a529452e1c4ff..ac8ca3531a0457d3a6e853a8cf51830cab284daa 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -3,7 +3,7 @@ module.exports = {
     "useTabs": true,
     "trailingComma": "all",
     "jsxBracketSameLine": true,
-    "importOrder": ["/(lib)", "/(redux)", "/(context)", "/(ui|common)|.svg$", "^[./]"],
+    "importOrder": ["preact|classnames|.scss$", "/(lib)", "/(redux)", "/(context)", "/(ui|common)|.svg$", "^[./]"],
     "importOrderSeparation": true,
 }
   
\ No newline at end of file
diff --git a/src/assets/emojis.ts b/src/assets/emojis.ts
index 33a777e346f92de8e140118f7cd03eaab74715f8..1f58692783f403bcf266a97a2885bf9549d2b120 100644
--- a/src/assets/emojis.ts
+++ b/src/assets/emojis.ts
@@ -1 +1,1850 @@
-export const emojiDictionary={"100":"💯","1234":"🔢","grinning":"😀","smiley":"😃","smile":"😄","grin":"😁","laughing":"😆","satisfied":"😆","sweat_smile":"😅","rofl":"🤣","joy":"😂","slightly_smiling_face":"🙂","upside_down_face":"🙃","wink":"😉","blush":"😊","innocent":"😇","smiling_face_with_three_hearts":"🥰","heart_eyes":"😍","star_struck":"🤩","kissing_heart":"😘","kissing":"😗","relaxed":"☺️","kissing_closed_eyes":"😚","kissing_smiling_eyes":"😙","smiling_face_with_tear":"🥲","yum":"😋","stuck_out_tongue":"😛","stuck_out_tongue_winking_eye":"😜","zany_face":"🤪","stuck_out_tongue_closed_eyes":"😝","money_mouth_face":"🤑","hugs":"🤗","hand_over_mouth":"🤭","shushing_face":"🤫","thinking":"🤔","zipper_mouth_face":"🤐","raised_eyebrow":"🤨","neutral_face":"😐","expressionless":"😑","no_mouth":"😶","smirk":"😏","unamused":"😒","roll_eyes":"🙄","grimacing":"😬","lying_face":"🤥","relieved":"😌","pensive":"😔","sleepy":"😪","drooling_face":"🤤","sleeping":"😴","mask":"😷","face_with_thermometer":"🤒","face_with_head_bandage":"🤕","nauseated_face":"🤢","vomiting_face":"🤮","sneezing_face":"🤧","hot_face":"🥵","cold_face":"🥶","woozy_face":"🥴","dizzy_face":"😵","exploding_head":"🤯","cowboy_hat_face":"🤠","partying_face":"🥳","disguised_face":"🥸","sunglasses":"😎","nerd_face":"🤓","monocle_face":"🧐","confused":"😕","worried":"😟","slightly_frowning_face":"🙁","frowning_face":"☹️","open_mouth":"😮","hushed":"😯","astonished":"😲","flushed":"😳","pleading_face":"🥺","frowning":"😦","anguished":"😧","fearful":"😨","cold_sweat":"😰","disappointed_relieved":"😥","cry":"😢","sob":"😭","scream":"😱","confounded":"😖","persevere":"😣","disappointed":"😞","sweat":"😓","weary":"😩","tired_face":"😫","yawning_face":"🥱","triumph":"😤","rage":"😡","pout":"😡","angry":"😠","cursing_face":"🤬","smiling_imp":"😈","imp":"👿","skull":"💀","skull_and_crossbones":"☠️","hankey":"💩","poop":"💩","shit":"💩","clown_face":"🤡","japanese_ogre":"👹","japanese_goblin":"👺","ghost":"👻","alien":"👽","space_invader":"👾","robot":"🤖","smiley_cat":"😺","smile_cat":"😸","joy_cat":"😹","heart_eyes_cat":"😻","smirk_cat":"😼","kissing_cat":"😽","scream_cat":"🙀","crying_cat_face":"😿","pouting_cat":"😾","see_no_evil":"🙈","hear_no_evil":"🙉","speak_no_evil":"🙊","kiss":"💋","love_letter":"💌","cupid":"💘","gift_heart":"💝","sparkling_heart":"💖","heartpulse":"💗","heartbeat":"💓","revolving_hearts":"💞","two_hearts":"💕","heart_decoration":"💟","heavy_heart_exclamation":"❣️","broken_heart":"💔","heart":"❤️","orange_heart":"🧡","yellow_heart":"💛","green_heart":"💚","blue_heart":"💙","purple_heart":"💜","brown_heart":"🤎","black_heart":"🖤","white_heart":"🤍","anger":"💢","boom":"💥","collision":"💥","dizzy":"💫","sweat_drops":"💦","dash":"💨","hole":"🕳️","bomb":"💣","speech_balloon":"💬","eye_speech_bubble":"👁️‍🗨️","left_speech_bubble":"🗨️","right_anger_bubble":"🗯️","thought_balloon":"💭","zzz":"💤","wave":"👋","raised_back_of_hand":"🤚","raised_hand_with_fingers_splayed":"🖐️","hand":"✋","raised_hand":"✋","vulcan_salute":"🖖","ok_hand":"👌","pinched_fingers":"🤌","pinching_hand":"🤏","v":"✌️","crossed_fingers":"🤞","love_you_gesture":"🤟","metal":"🤘","call_me_hand":"🤙","point_left":"👈","point_right":"👉","point_up_2":"👆","middle_finger":"🖕","fu":"🖕","point_down":"👇","point_up":"☝️","+1":"👍","thumbsup":"👍","-1":"👎","thumbsdown":"👎","fist_raised":"✊","fist":"✊","fist_oncoming":"👊","facepunch":"👊","punch":"👊","fist_left":"🤛","fist_right":"🤜","clap":"👏","raised_hands":"🙌","open_hands":"👐","palms_up_together":"🤲","handshake":"🤝","pray":"🙏","writing_hand":"✍️","nail_care":"💅","selfie":"🤳","muscle":"💪","mechanical_arm":"🦾","mechanical_leg":"🦿","leg":"🦵","foot":"🦶","ear":"👂","ear_with_hearing_aid":"🦻","nose":"👃","brain":"🧠","anatomical_heart":"🫀","lungs":"🫁","tooth":"🦷","bone":"🦴","eyes":"👀","eye":"👁️","tongue":"👅","lips":"👄","baby":"👶","child":"🧒","boy":"👦","girl":"👧","adult":"🧑","blond_haired_person":"👱","man":"👨","bearded_person":"🧔","red_haired_man":"👨‍🦰","curly_haired_man":"👨‍🦱","white_haired_man":"👨‍🦳","bald_man":"👨‍🦲","woman":"👩","red_haired_woman":"👩‍🦰","person_red_hair":"🧑‍🦰","curly_haired_woman":"👩‍🦱","person_curly_hair":"🧑‍🦱","white_haired_woman":"👩‍🦳","person_white_hair":"🧑‍🦳","bald_woman":"👩‍🦲","person_bald":"🧑‍🦲","blond_haired_woman":"👱‍♀️","blonde_woman":"👱‍♀️","blond_haired_man":"👱‍♂️","older_adult":"🧓","older_man":"👴","older_woman":"👵","frowning_person":"🙍","frowning_man":"🙍‍♂️","frowning_woman":"🙍‍♀️","pouting_face":"🙎","pouting_man":"🙎‍♂️","pouting_woman":"🙎‍♀️","no_good":"🙅","no_good_man":"🙅‍♂️","ng_man":"🙅‍♂️","no_good_woman":"🙅‍♀️","ng_woman":"🙅‍♀️","ok_person":"🙆","ok_man":"🙆‍♂️","ok_woman":"🙆‍♀️","tipping_hand_person":"💁","information_desk_person":"💁","tipping_hand_man":"💁‍♂️","sassy_man":"💁‍♂️","tipping_hand_woman":"💁‍♀️","sassy_woman":"💁‍♀️","raising_hand":"🙋","raising_hand_man":"🙋‍♂️","raising_hand_woman":"🙋‍♀️","deaf_person":"🧏","deaf_man":"🧏‍♂️","deaf_woman":"🧏‍♀️","bow":"🙇","bowing_man":"🙇‍♂️","bowing_woman":"🙇‍♀️","facepalm":"🤦","man_facepalming":"🤦‍♂️","woman_facepalming":"🤦‍♀️","shrug":"🤷","man_shrugging":"🤷‍♂️","woman_shrugging":"🤷‍♀️","health_worker":"🧑‍⚕️","man_health_worker":"👨‍⚕️","woman_health_worker":"👩‍⚕️","student":"🧑‍🎓","man_student":"👨‍🎓","woman_student":"👩‍🎓","teacher":"🧑‍🏫","man_teacher":"👨‍🏫","woman_teacher":"👩‍🏫","judge":"🧑‍⚖️","man_judge":"👨‍⚖️","woman_judge":"👩‍⚖️","farmer":"🧑‍🌾","man_farmer":"👨‍🌾","woman_farmer":"👩‍🌾","cook":"🧑‍🍳","man_cook":"👨‍🍳","woman_cook":"👩‍🍳","mechanic":"🧑‍🔧","man_mechanic":"👨‍🔧","woman_mechanic":"👩‍🔧","factory_worker":"🧑‍🏭","man_factory_worker":"👨‍🏭","woman_factory_worker":"👩‍🏭","office_worker":"🧑‍💼","man_office_worker":"👨‍💼","woman_office_worker":"👩‍💼","scientist":"🧑‍🔬","man_scientist":"👨‍🔬","woman_scientist":"👩‍🔬","technologist":"🧑‍💻","man_technologist":"👨‍💻","woman_technologist":"👩‍💻","singer":"🧑‍🎤","man_singer":"👨‍🎤","woman_singer":"👩‍🎤","artist":"🧑‍🎨","man_artist":"👨‍🎨","woman_artist":"👩‍🎨","pilot":"🧑‍✈️","man_pilot":"👨‍✈️","woman_pilot":"👩‍✈️","astronaut":"🧑‍🚀","man_astronaut":"👨‍🚀","woman_astronaut":"👩‍🚀","firefighter":"🧑‍🚒","man_firefighter":"👨‍🚒","woman_firefighter":"👩‍🚒","police_officer":"👮","cop":"👮","policeman":"👮‍♂️","policewoman":"👮‍♀️","detective":"🕵️","male_detective":"🕵️‍♂️","female_detective":"🕵️‍♀️","guard":"💂","guardsman":"💂‍♂️","guardswoman":"💂‍♀️","ninja":"🥷","construction_worker":"👷","construction_worker_man":"👷‍♂️","construction_worker_woman":"👷‍♀️","prince":"🤴","princess":"👸","person_with_turban":"👳","man_with_turban":"👳‍♂️","woman_with_turban":"👳‍♀️","man_with_gua_pi_mao":"👲","woman_with_headscarf":"🧕","person_in_tuxedo":"🤵","man_in_tuxedo":"🤵‍♂️","woman_in_tuxedo":"🤵‍♀️","person_with_veil":"👰","man_with_veil":"👰‍♂️","woman_with_veil":"👰‍♀️","bride_with_veil":"👰‍♀️","pregnant_woman":"🤰","breast_feeding":"🤱","woman_feeding_baby":"👩‍🍼","man_feeding_baby":"👨‍🍼","person_feeding_baby":"🧑‍🍼","angel":"👼","santa":"🎅","mrs_claus":"🤶","mx_claus":"🧑‍🎄","superhero":"🦸","superhero_man":"🦸‍♂️","superhero_woman":"🦸‍♀️","supervillain":"🦹","supervillain_man":"🦹‍♂️","supervillain_woman":"🦹‍♀️","mage":"🧙","mage_man":"🧙‍♂️","mage_woman":"🧙‍♀️","fairy":"🧚","fairy_man":"🧚‍♂️","fairy_woman":"🧚‍♀️","vampire":"🧛","vampire_man":"🧛‍♂️","vampire_woman":"🧛‍♀️","merperson":"🧜","merman":"🧜‍♂️","mermaid":"🧜‍♀️","elf":"🧝","elf_man":"🧝‍♂️","elf_woman":"🧝‍♀️","genie":"🧞","genie_man":"🧞‍♂️","genie_woman":"🧞‍♀️","zombie":"🧟","zombie_man":"🧟‍♂️","zombie_woman":"🧟‍♀️","massage":"💆","massage_man":"💆‍♂️","massage_woman":"💆‍♀️","haircut":"💇","haircut_man":"💇‍♂️","haircut_woman":"💇‍♀️","walking":"🚶","walking_man":"🚶‍♂️","walking_woman":"🚶‍♀️","standing_person":"🧍","standing_man":"🧍‍♂️","standing_woman":"🧍‍♀️","kneeling_person":"🧎","kneeling_man":"🧎‍♂️","kneeling_woman":"🧎‍♀️","person_with_probing_cane":"🧑‍🦯","man_with_probing_cane":"👨‍🦯","woman_with_probing_cane":"👩‍🦯","person_in_motorized_wheelchair":"🧑‍🦼","man_in_motorized_wheelchair":"👨‍🦼","woman_in_motorized_wheelchair":"👩‍🦼","person_in_manual_wheelchair":"🧑‍🦽","man_in_manual_wheelchair":"👨‍🦽","woman_in_manual_wheelchair":"👩‍🦽","runner":"🏃","running":"🏃","running_man":"🏃‍♂️","running_woman":"🏃‍♀️","woman_dancing":"💃","dancer":"💃","man_dancing":"🕺","business_suit_levitating":"🕴️","dancers":"👯","dancing_men":"👯‍♂️","dancing_women":"👯‍♀️","sauna_person":"🧖","sauna_man":"🧖‍♂️","sauna_woman":"🧖‍♀️","climbing":"🧗","climbing_man":"🧗‍♂️","climbing_woman":"🧗‍♀️","person_fencing":"🤺","horse_racing":"🏇","skier":"⛷️","snowboarder":"🏂","golfing":"🏌️","golfing_man":"🏌️‍♂️","golfing_woman":"🏌️‍♀️","surfer":"🏄","surfing_man":"🏄‍♂️","surfing_woman":"🏄‍♀️","rowboat":"🚣","rowing_man":"🚣‍♂️","rowing_woman":"🚣‍♀️","swimmer":"🏊","swimming_man":"🏊‍♂️","swimming_woman":"🏊‍♀️","bouncing_ball_person":"⛹️","bouncing_ball_man":"⛹️‍♂️","basketball_man":"⛹️‍♂️","bouncing_ball_woman":"⛹️‍♀️","basketball_woman":"⛹️‍♀️","weight_lifting":"🏋️","weight_lifting_man":"🏋️‍♂️","weight_lifting_woman":"🏋️‍♀️","bicyclist":"🚴","biking_man":"🚴‍♂️","biking_woman":"🚴‍♀️","mountain_bicyclist":"🚵","mountain_biking_man":"🚵‍♂️","mountain_biking_woman":"🚵‍♀️","cartwheeling":"🤸","man_cartwheeling":"🤸‍♂️","woman_cartwheeling":"🤸‍♀️","wrestling":"🤼","men_wrestling":"🤼‍♂️","women_wrestling":"🤼‍♀️","water_polo":"🤽","man_playing_water_polo":"🤽‍♂️","woman_playing_water_polo":"🤽‍♀️","handball_person":"🤾","man_playing_handball":"🤾‍♂️","woman_playing_handball":"🤾‍♀️","juggling_person":"🤹","man_juggling":"🤹‍♂️","woman_juggling":"🤹‍♀️","lotus_position":"🧘","lotus_position_man":"🧘‍♂️","lotus_position_woman":"🧘‍♀️","bath":"🛀","sleeping_bed":"🛌","people_holding_hands":"🧑‍🤝‍🧑","two_women_holding_hands":"👭","couple":"👫","two_men_holding_hands":"👬","couplekiss":"💏","couplekiss_man_woman":"👩‍❤️‍💋‍👨","couplekiss_man_man":"👨‍❤️‍💋‍👨","couplekiss_woman_woman":"👩‍❤️‍💋‍👩","couple_with_heart":"💑","couple_with_heart_woman_man":"👩‍❤️‍👨","couple_with_heart_man_man":"👨‍❤️‍👨","couple_with_heart_woman_woman":"👩‍❤️‍👩","family":"👪","family_man_woman_boy":"👨‍👩‍👦","family_man_woman_girl":"👨‍👩‍👧","family_man_woman_girl_boy":"👨‍👩‍👧‍👦","family_man_woman_boy_boy":"👨‍👩‍👦‍👦","family_man_woman_girl_girl":"👨‍👩‍👧‍👧","family_man_man_boy":"👨‍👨‍👦","family_man_man_girl":"👨‍👨‍👧","family_man_man_girl_boy":"👨‍👨‍👧‍👦","family_man_man_boy_boy":"👨‍👨‍👦‍👦","family_man_man_girl_girl":"👨‍👨‍👧‍👧","family_woman_woman_boy":"👩‍👩‍👦","family_woman_woman_girl":"👩‍👩‍👧","family_woman_woman_girl_boy":"👩‍👩‍👧‍👦","family_woman_woman_boy_boy":"👩‍👩‍👦‍👦","family_woman_woman_girl_girl":"👩‍👩‍👧‍👧","family_man_boy":"👨‍👦","family_man_boy_boy":"👨‍👦‍👦","family_man_girl":"👨‍👧","family_man_girl_boy":"👨‍👧‍👦","family_man_girl_girl":"👨‍👧‍👧","family_woman_boy":"👩‍👦","family_woman_boy_boy":"👩‍👦‍👦","family_woman_girl":"👩‍👧","family_woman_girl_boy":"👩‍👧‍👦","family_woman_girl_girl":"👩‍👧‍👧","speaking_head":"🗣️","bust_in_silhouette":"👤","busts_in_silhouette":"👥","people_hugging":"🫂","footprints":"👣","monkey_face":"🐵","monkey":"🐒","gorilla":"🦍","orangutan":"🦧","dog":"🐶","dog2":"🐕","guide_dog":"🦮","service_dog":"🐕‍🦺","poodle":"🐩","wolf":"🐺","fox_face":"🦊","raccoon":"🦝","cat":"🐱","cat2":"🐈","black_cat":"🐈‍⬛","lion":"🦁","tiger":"🐯","tiger2":"🐅","leopard":"🐆","horse":"🐴","racehorse":"🐎","unicorn":"🦄","zebra":"🦓","deer":"🦌","bison":"🦬","cow":"🐮","ox":"🐂","water_buffalo":"🐃","cow2":"🐄","pig":"🐷","pig2":"🐖","boar":"🐗","pig_nose":"🐽","ram":"🐏","sheep":"🐑","goat":"🐐","dromedary_camel":"🐪","camel":"🐫","llama":"🦙","giraffe":"🦒","elephant":"🐘","mammoth":"🦣","rhinoceros":"🦏","hippopotamus":"🦛","mouse":"🐭","mouse2":"🐁","rat":"🐀","hamster":"🐹","rabbit":"🐰","rabbit2":"🐇","chipmunk":"🐿️","beaver":"🦫","hedgehog":"🦔","bat":"🦇","bear":"🐻","polar_bear":"🐻‍❄️","koala":"🐨","panda_face":"🐼","sloth":"🦥","otter":"🦦","skunk":"🦨","kangaroo":"🦘","badger":"🦡","feet":"🐾","paw_prints":"🐾","turkey":"🦃","chicken":"🐔","rooster":"🐓","hatching_chick":"🐣","baby_chick":"🐤","hatched_chick":"🐥","bird":"🐦","penguin":"🐧","dove":"🕊️","eagle":"🦅","duck":"🦆","swan":"🦢","owl":"🦉","dodo":"🦤","feather":"🪶","flamingo":"🦩","peacock":"🦚","parrot":"🦜","frog":"🐸","crocodile":"🐊","turtle":"🐢","lizard":"🦎","snake":"🐍","dragon_face":"🐲","dragon":"🐉","sauropod":"🦕","t-rex":"🦖","whale":"🐳","whale2":"🐋","dolphin":"🐬","flipper":"🐬","seal":"🦭","fish":"🐟","tropical_fish":"🐠","blowfish":"🐡","shark":"🦈","octopus":"🐙","shell":"🐚","snail":"🐌","butterfly":"🦋","bug":"🐛","ant":"🐜","bee":"🐝","honeybee":"🐝","beetle":"🪲","lady_beetle":"🐞","cricket":"🦗","cockroach":"🪳","spider":"🕷️","spider_web":"🕸️","scorpion":"🦂","mosquito":"🦟","fly":"🪰","worm":"🪱","microbe":"🦠","bouquet":"💐","cherry_blossom":"🌸","white_flower":"💮","rosette":"🏵️","rose":"🌹","wilted_flower":"🥀","hibiscus":"🌺","sunflower":"🌻","blossom":"🌼","tulip":"🌷","seedling":"🌱","potted_plant":"🪴","evergreen_tree":"🌲","deciduous_tree":"🌳","palm_tree":"🌴","cactus":"🌵","ear_of_rice":"🌾","herb":"🌿","shamrock":"☘️","four_leaf_clover":"🍀","maple_leaf":"🍁","fallen_leaf":"🍂","leaves":"🍃","grapes":"🍇","melon":"🍈","watermelon":"🍉","tangerine":"🍊","orange":"🍊","mandarin":"🍊","lemon":"🍋","banana":"🍌","pineapple":"🍍","mango":"🥭","apple":"🍎","green_apple":"🍏","pear":"🍐","peach":"🍑","cherries":"🍒","strawberry":"🍓","blueberries":"🫐","kiwi_fruit":"🥝","tomato":"🍅","olive":"🫒","coconut":"🥥","avocado":"🥑","eggplant":"🍆","potato":"🥔","carrot":"🥕","corn":"🌽","hot_pepper":"🌶️","bell_pepper":"🫑","cucumber":"🥒","leafy_green":"🥬","broccoli":"🥦","garlic":"🧄","onion":"🧅","mushroom":"🍄","peanuts":"🥜","chestnut":"🌰","bread":"🍞","croissant":"🥐","baguette_bread":"🥖","flatbread":"🫓","pretzel":"🥨","bagel":"🥯","pancakes":"🥞","waffle":"🧇","cheese":"🧀","meat_on_bone":"🍖","poultry_leg":"🍗","cut_of_meat":"🥩","bacon":"🥓","hamburger":"🍔","fries":"🍟","pizza":"🍕","hotdog":"🌭","sandwich":"🥪","taco":"🌮","burrito":"🌯","tamale":"🫔","stuffed_flatbread":"🥙","falafel":"🧆","egg":"🥚","fried_egg":"🍳","shallow_pan_of_food":"🥘","stew":"🍲","fondue":"🫕","bowl_with_spoon":"🥣","green_salad":"🥗","popcorn":"🍿","butter":"🧈","salt":"🧂","canned_food":"🥫","bento":"🍱","rice_cracker":"🍘","rice_ball":"🍙","rice":"🍚","curry":"🍛","ramen":"🍜","spaghetti":"🍝","sweet_potato":"🍠","oden":"🍢","sushi":"🍣","fried_shrimp":"🍤","fish_cake":"🍥","moon_cake":"🥮","dango":"🍡","dumpling":"🥟","fortune_cookie":"🥠","takeout_box":"🥡","crab":"🦀","lobster":"🦞","shrimp":"🦐","squid":"🦑","oyster":"🦪","icecream":"🍦","shaved_ice":"🍧","ice_cream":"🍨","doughnut":"🍩","cookie":"🍪","birthday":"🎂","cake":"🍰","cupcake":"🧁","pie":"🥧","chocolate_bar":"🍫","candy":"🍬","lollipop":"🍭","custard":"🍮","honey_pot":"🍯","baby_bottle":"🍼","milk_glass":"🥛","coffee":"☕","teapot":"🫖","tea":"🍵","sake":"🍶","champagne":"🍾","wine_glass":"🍷","cocktail":"🍸","tropical_drink":"🍹","beer":"🍺","beers":"🍻","clinking_glasses":"🥂","tumbler_glass":"🥃","cup_with_straw":"🥤","bubble_tea":"🧋","beverage_box":"🧃","mate":"🧉","ice_cube":"🧊","chopsticks":"🥢","plate_with_cutlery":"🍽️","fork_and_knife":"🍴","spoon":"🥄","hocho":"🔪","knife":"🔪","amphora":"🏺","earth_africa":"🌍","earth_americas":"🌎","earth_asia":"🌏","globe_with_meridians":"🌐","world_map":"🗺️","japan":"🗾","compass":"🧭","mountain_snow":"🏔️","mountain":"⛰️","volcano":"🌋","mount_fuji":"🗻","camping":"🏕️","beach_umbrella":"🏖️","desert":"🏜️","desert_island":"🏝️","national_park":"🏞️","stadium":"🏟️","classical_building":"🏛️","building_construction":"🏗️","bricks":"🧱","rock":"🪨","wood":"🪵","hut":"🛖","houses":"🏘️","derelict_house":"🏚️","house":"🏠","house_with_garden":"🏡","office":"🏢","post_office":"🏣","european_post_office":"🏤","hospital":"🏥","bank":"🏦","hotel":"🏨","love_hotel":"🏩","convenience_store":"🏪","school":"🏫","department_store":"🏬","factory":"🏭","japanese_castle":"🏯","european_castle":"🏰","wedding":"💒","tokyo_tower":"🗼","statue_of_liberty":"🗽","church":"⛪","mosque":"🕌","hindu_temple":"🛕","synagogue":"🕍","shinto_shrine":"⛩️","kaaba":"🕋","fountain":"⛲","tent":"⛺","foggy":"🌁","night_with_stars":"🌃","cityscape":"🏙️","sunrise_over_mountains":"🌄","sunrise":"🌅","city_sunset":"🌆","city_sunrise":"🌇","bridge_at_night":"🌉","hotsprings":"♨️","carousel_horse":"🎠","ferris_wheel":"🎡","roller_coaster":"🎢","barber":"💈","circus_tent":"🎪","steam_locomotive":"🚂","railway_car":"🚃","bullettrain_side":"🚄","bullettrain_front":"🚅","train2":"🚆","metro":"🚇","light_rail":"🚈","station":"🚉","tram":"🚊","monorail":"🚝","mountain_railway":"🚞","train":"🚋","bus":"🚌","oncoming_bus":"🚍","trolleybus":"🚎","minibus":"🚐","ambulance":"🚑","fire_engine":"🚒","police_car":"🚓","oncoming_police_car":"🚔","taxi":"🚕","oncoming_taxi":"🚖","car":"🚗","red_car":"🚗","oncoming_automobile":"🚘","blue_car":"🚙","pickup_truck":"🛻","truck":"🚚","articulated_lorry":"🚛","tractor":"🚜","racing_car":"🏎️","motorcycle":"🏍️","motor_scooter":"🛵","manual_wheelchair":"🦽","motorized_wheelchair":"🦼","auto_rickshaw":"🛺","bike":"🚲","kick_scooter":"🛴","skateboard":"🛹","roller_skate":"🛼","busstop":"🚏","motorway":"🛣️","railway_track":"🛤️","oil_drum":"🛢️","fuelpump":"⛽","rotating_light":"🚨","traffic_light":"🚥","vertical_traffic_light":"🚦","stop_sign":"🛑","construction":"🚧","anchor":"⚓","boat":"⛵","sailboat":"⛵","canoe":"🛶","speedboat":"🚤","passenger_ship":"🛳️","ferry":"⛴️","motor_boat":"🛥️","ship":"🚢","airplane":"✈️","small_airplane":"🛩️","flight_departure":"🛫","flight_arrival":"🛬","parachute":"🪂","seat":"💺","helicopter":"🚁","suspension_railway":"🚟","mountain_cableway":"🚠","aerial_tramway":"🚡","artificial_satellite":"🛰️","rocket":"🚀","flying_saucer":"🛸","bellhop_bell":"🛎️","luggage":"🧳","hourglass":"⌛","hourglass_flowing_sand":"⏳","watch":"⌚","alarm_clock":"⏰","stopwatch":"⏱️","timer_clock":"⏲️","mantelpiece_clock":"🕰️","clock12":"🕛","clock1230":"🕧","clock1":"🕐","clock130":"🕜","clock2":"🕑","clock230":"🕝","clock3":"🕒","clock330":"🕞","clock4":"🕓","clock430":"🕟","clock5":"🕔","clock530":"🕠","clock6":"🕕","clock630":"🕡","clock7":"🕖","clock730":"🕢","clock8":"🕗","clock830":"🕣","clock9":"🕘","clock930":"🕤","clock10":"🕙","clock1030":"🕥","clock11":"🕚","clock1130":"🕦","new_moon":"🌑","waxing_crescent_moon":"🌒","first_quarter_moon":"🌓","moon":"🌔","waxing_gibbous_moon":"🌔","full_moon":"🌕","waning_gibbous_moon":"🌖","last_quarter_moon":"🌗","waning_crescent_moon":"🌘","crescent_moon":"🌙","new_moon_with_face":"🌚","first_quarter_moon_with_face":"🌛","last_quarter_moon_with_face":"🌜","thermometer":"🌡️","sunny":"☀️","full_moon_with_face":"🌝","sun_with_face":"🌞","ringed_planet":"🪐","star":"⭐","star2":"🌟","stars":"🌠","milky_way":"🌌","cloud":"☁️","partly_sunny":"⛅","cloud_with_lightning_and_rain":"⛈️","sun_behind_small_cloud":"🌤️","sun_behind_large_cloud":"🌥️","sun_behind_rain_cloud":"🌦️","cloud_with_rain":"🌧️","cloud_with_snow":"🌨️","cloud_with_lightning":"🌩️","tornado":"🌪️","fog":"🌫️","wind_face":"🌬️","cyclone":"🌀","rainbow":"🌈","closed_umbrella":"🌂","open_umbrella":"☂️","umbrella":"☔","parasol_on_ground":"⛱️","zap":"⚡","snowflake":"❄️","snowman_with_snow":"☃️","snowman":"⛄","comet":"☄️","fire":"🔥","droplet":"💧","ocean":"🌊","jack_o_lantern":"🎃","christmas_tree":"🎄","fireworks":"🎆","sparkler":"🎇","firecracker":"🧨","sparkles":"✨","balloon":"🎈","tada":"🎉","confetti_ball":"🎊","tanabata_tree":"🎋","bamboo":"🎍","dolls":"🎎","flags":"🎏","wind_chime":"🎐","rice_scene":"🎑","red_envelope":"🧧","ribbon":"🎀","gift":"🎁","reminder_ribbon":"🎗️","tickets":"🎟️","ticket":"🎫","medal_military":"🎖️","trophy":"🏆","medal_sports":"🏅","1st_place_medal":"🥇","2nd_place_medal":"🥈","3rd_place_medal":"🥉","soccer":"⚽","baseball":"⚾","softball":"🥎","basketball":"🏀","volleyball":"🏐","football":"🏈","rugby_football":"🏉","tennis":"🎾","flying_disc":"🥏","bowling":"🎳","cricket_game":"🏏","field_hockey":"🏑","ice_hockey":"🏒","lacrosse":"🥍","ping_pong":"🏓","badminton":"🏸","boxing_glove":"🥊","martial_arts_uniform":"🥋","goal_net":"🥅","golf":"⛳","ice_skate":"⛸️","fishing_pole_and_fish":"🎣","diving_mask":"🤿","running_shirt_with_sash":"🎽","ski":"🎿","sled":"🛷","curling_stone":"🥌","dart":"🎯","yo_yo":"🪀","kite":"🪁","8ball":"🎱","crystal_ball":"🔮","magic_wand":"🪄","nazar_amulet":"🧿","video_game":"🎮","joystick":"🕹️","slot_machine":"🎰","game_die":"🎲","jigsaw":"🧩","teddy_bear":"🧸","pinata":"🪅","nesting_dolls":"🪆","spades":"♠️","hearts":"♥️","diamonds":"♦️","clubs":"♣️","chess_pawn":"♟️","black_joker":"🃏","mahjong":"🀄","flower_playing_cards":"🎴","performing_arts":"🎭","framed_picture":"🖼️","art":"🎨","thread":"🧵","sewing_needle":"🪡","yarn":"🧶","knot":"🪢","eyeglasses":"👓","dark_sunglasses":"🕶️","goggles":"🥽","lab_coat":"🥼","safety_vest":"🦺","necktie":"👔","shirt":"👕","tshirt":"👕","jeans":"👖","scarf":"🧣","gloves":"🧤","coat":"🧥","socks":"🧦","dress":"👗","kimono":"👘","sari":"🥻","one_piece_swimsuit":"🩱","swim_brief":"🩲","shorts":"🩳","bikini":"👙","womans_clothes":"👚","purse":"👛","handbag":"👜","pouch":"👝","shopping":"🛍️","school_satchel":"🎒","thong_sandal":"🩴","mans_shoe":"👞","shoe":"👞","athletic_shoe":"👟","hiking_boot":"🥾","flat_shoe":"🥿","high_heel":"👠","sandal":"👡","ballet_shoes":"🩰","boot":"👢","crown":"👑","womans_hat":"👒","tophat":"🎩","mortar_board":"🎓","billed_cap":"🧢","military_helmet":"🪖","rescue_worker_helmet":"⛑️","prayer_beads":"📿","lipstick":"💄","ring":"💍","gem":"💎","mute":"🔇","speaker":"🔈","sound":"🔉","loud_sound":"🔊","loudspeaker":"📢","mega":"📣","postal_horn":"📯","bell":"🔔","no_bell":"🔕","musical_score":"🎼","musical_note":"🎵","notes":"🎶","studio_microphone":"🎙️","level_slider":"🎚️","control_knobs":"🎛️","microphone":"🎤","headphones":"🎧","radio":"📻","saxophone":"🎷","accordion":"🪗","guitar":"🎸","musical_keyboard":"🎹","trumpet":"🎺","violin":"🎻","banjo":"🪕","drum":"🥁","long_drum":"🪘","iphone":"📱","calling":"📲","phone":"☎️","telephone":"☎️","telephone_receiver":"📞","pager":"📟","fax":"📠","battery":"🔋","electric_plug":"🔌","computer":"💻","desktop_computer":"🖥️","printer":"🖨️","keyboard":"⌨️","computer_mouse":"🖱️","trackball":"🖲️","minidisc":"💽","floppy_disk":"💾","cd":"💿","dvd":"📀","abacus":"🧮","movie_camera":"🎥","film_strip":"🎞️","film_projector":"📽️","clapper":"🎬","tv":"📺","camera":"📷","camera_flash":"📸","video_camera":"📹","vhs":"📼","mag":"🔍","mag_right":"🔎","candle":"🕯️","bulb":"💡","flashlight":"🔦","izakaya_lantern":"🏮","lantern":"🏮","diya_lamp":"🪔","notebook_with_decorative_cover":"📔","closed_book":"📕","book":"📖","open_book":"📖","green_book":"📗","blue_book":"📘","orange_book":"📙","books":"📚","notebook":"📓","ledger":"📒","page_with_curl":"📃","scroll":"📜","page_facing_up":"📄","newspaper":"📰","newspaper_roll":"🗞️","bookmark_tabs":"📑","bookmark":"🔖","label":"🏷️","moneybag":"💰","coin":"🪙","yen":"💴","dollar":"💵","euro":"💶","pound":"💷","money_with_wings":"💸","credit_card":"💳","receipt":"🧾","chart":"💹","envelope":"✉️","email":"📧","e-mail":"📧","incoming_envelope":"📨","envelope_with_arrow":"📩","outbox_tray":"📤","inbox_tray":"📥","package":"📦","mailbox":"📫","mailbox_closed":"📪","mailbox_with_mail":"📬","mailbox_with_no_mail":"📭","postbox":"📮","ballot_box":"🗳️","pencil2":"✏️","black_nib":"✒️","fountain_pen":"🖋️","pen":"🖊️","paintbrush":"🖌️","crayon":"🖍️","memo":"📝","pencil":"📝","briefcase":"💼","file_folder":"📁","open_file_folder":"📂","card_index_dividers":"🗂️","date":"📅","calendar":"📆","spiral_notepad":"🗒️","spiral_calendar":"🗓️","card_index":"📇","chart_with_upwards_trend":"📈","chart_with_downwards_trend":"📉","bar_chart":"📊","clipboard":"📋","pushpin":"📌","round_pushpin":"📍","paperclip":"📎","paperclips":"🖇️","straight_ruler":"📏","triangular_ruler":"📐","scissors":"✂️","card_file_box":"🗃️","file_cabinet":"🗄️","wastebasket":"🗑️","lock":"🔒","unlock":"🔓","lock_with_ink_pen":"🔏","closed_lock_with_key":"🔐","key":"🔑","old_key":"🗝️","hammer":"🔨","axe":"🪓","pick":"⛏️","hammer_and_pick":"⚒️","hammer_and_wrench":"🛠️","dagger":"🗡️","crossed_swords":"⚔️","gun":"🔫","boomerang":"🪃","bow_and_arrow":"🏹","shield":"🛡️","carpentry_saw":"🪚","wrench":"🔧","screwdriver":"🪛","nut_and_bolt":"🔩","gear":"⚙️","clamp":"🗜️","balance_scale":"⚖️","probing_cane":"🦯","link":"🔗","chains":"⛓️","hook":"🪝","toolbox":"🧰","magnet":"🧲","ladder":"🪜","alembic":"⚗️","test_tube":"🧪","petri_dish":"🧫","dna":"🧬","microscope":"🔬","telescope":"🔭","satellite":"📡","syringe":"💉","drop_of_blood":"🩸","pill":"💊","adhesive_bandage":"🩹","stethoscope":"🩺","door":"🚪","elevator":"🛗","mirror":"🪞","window":"🪟","bed":"🛏️","couch_and_lamp":"🛋️","chair":"🪑","toilet":"🚽","plunger":"🪠","shower":"🚿","bathtub":"🛁","mouse_trap":"🪤","razor":"🪒","lotion_bottle":"🧴","safety_pin":"🧷","broom":"🧹","basket":"🧺","roll_of_paper":"🧻","bucket":"🪣","soap":"🧼","toothbrush":"🪥","sponge":"🧽","fire_extinguisher":"🧯","shopping_cart":"🛒","smoking":"🚬","coffin":"⚰️","headstone":"🪦","funeral_urn":"⚱️","moyai":"🗿","placard":"🪧","atm":"🏧","put_litter_in_its_place":"🚮","potable_water":"🚰","wheelchair":"♿","mens":"🚹","womens":"🚺","restroom":"🚻","baby_symbol":"🚼","wc":"🚾","passport_control":"🛂","customs":"🛃","baggage_claim":"🛄","left_luggage":"🛅","warning":"⚠️","children_crossing":"🚸","no_entry":"⛔","no_entry_sign":"🚫","no_bicycles":"🚳","no_smoking":"🚭","do_not_litter":"🚯","non-potable_water":"🚱","no_pedestrians":"🚷","no_mobile_phones":"📵","underage":"🔞","radioactive":"☢️","biohazard":"☣️","arrow_up":"⬆️","arrow_upper_right":"↗️","arrow_right":"➡️","arrow_lower_right":"↘️","arrow_down":"⬇️","arrow_lower_left":"↙️","arrow_left":"⬅️","arrow_upper_left":"↖️","arrow_up_down":"↕️","left_right_arrow":"↔️","leftwards_arrow_with_hook":"↩️","arrow_right_hook":"↪️","arrow_heading_up":"⤴️","arrow_heading_down":"⤵️","arrows_clockwise":"🔃","arrows_counterclockwise":"🔄","back":"🔙","end":"🔚","on":"🔛","soon":"🔜","top":"🔝","place_of_worship":"🛐","atom_symbol":"⚛️","om":"🕉️","star_of_david":"✡️","wheel_of_dharma":"☸️","yin_yang":"☯️","latin_cross":"✝️","orthodox_cross":"☦️","star_and_crescent":"☪️","peace_symbol":"☮️","menorah":"🕎","six_pointed_star":"🔯","aries":"♈","taurus":"♉","gemini":"♊","cancer":"♋","leo":"♌","virgo":"♍","libra":"♎","scorpius":"♏","sagittarius":"♐","capricorn":"♑","aquarius":"♒","pisces":"♓","ophiuchus":"⛎","twisted_rightwards_arrows":"🔀","repeat":"🔁","repeat_one":"🔂","arrow_forward":"▶️","fast_forward":"⏩","next_track_button":"⏭️","play_or_pause_button":"⏯️","arrow_backward":"◀️","rewind":"⏪","previous_track_button":"⏮️","arrow_up_small":"🔼","arrow_double_up":"⏫","arrow_down_small":"🔽","arrow_double_down":"⏬","pause_button":"⏸️","stop_button":"⏹️","record_button":"⏺️","eject_button":"⏏️","cinema":"🎦","low_brightness":"🔅","high_brightness":"🔆","signal_strength":"📶","vibration_mode":"📳","mobile_phone_off":"📴","female_sign":"♀️","male_sign":"♂️","transgender_symbol":"⚧️","heavy_multiplication_x":"✖️","heavy_plus_sign":"➕","heavy_minus_sign":"➖","heavy_division_sign":"➗","infinity":"♾️","bangbang":"‼️","interrobang":"⁉️","question":"❓","grey_question":"❔","grey_exclamation":"❕","exclamation":"❗","heavy_exclamation_mark":"❗","wavy_dash":"〰️","currency_exchange":"💱","heavy_dollar_sign":"💲","medical_symbol":"⚕️","recycle":"♻️","fleur_de_lis":"⚜️","trident":"🔱","name_badge":"📛","beginner":"🔰","o":"⭕","white_check_mark":"✅","ballot_box_with_check":"☑️","heavy_check_mark":"✔️","x":"❌","negative_squared_cross_mark":"❎","curly_loop":"➰","loop":"➿","part_alternation_mark":"〽️","eight_spoked_asterisk":"✳️","eight_pointed_black_star":"✴️","sparkle":"❇️","copyright":"©️","registered":"®️","tm":"™️","hash":"#️⃣","asterisk":"*️⃣","zero":"0️⃣","one":"1️⃣","two":"2️⃣","three":"3️⃣","four":"4️⃣","five":"5️⃣","six":"6️⃣","seven":"7️⃣","eight":"8️⃣","nine":"9️⃣","keycap_ten":"🔟","capital_abcd":"🔠","abcd":"🔡","symbols":"🔣","abc":"🔤","a":"🅰️","ab":"🆎","b":"🅱️","cl":"🆑","cool":"🆒","free":"🆓","information_source":"ℹ️","id":"🆔","m":"Ⓜ️","new":"🆕","ng":"🆖","o2":"🅾️","ok":"🆗","parking":"🅿️","sos":"🆘","up":"🆙","vs":"🆚","koko":"🈁","sa":"🈂️","u6708":"🈷️","u6709":"🈶","u6307":"🈯","ideograph_advantage":"🉐","u5272":"🈹","u7121":"🈚","u7981":"🈲","accept":"🉑","u7533":"🈸","u5408":"🈴","u7a7a":"🈳","congratulations":"㊗️","secret":"㊙️","u55b6":"🈺","u6e80":"🈵","red_circle":"🔴","orange_circle":"🟠","yellow_circle":"🟡","green_circle":"🟢","large_blue_circle":"🔵","purple_circle":"🟣","brown_circle":"🟤","black_circle":"⚫","white_circle":"⚪","red_square":"🟥","orange_square":"🟧","yellow_square":"🟨","green_square":"🟩","blue_square":"🟦","purple_square":"🟪","brown_square":"🟫","black_large_square":"⬛","white_large_square":"⬜","black_medium_square":"◼️","white_medium_square":"◻️","black_medium_small_square":"◾","white_medium_small_square":"◽","black_small_square":"▪️","white_small_square":"▫️","large_orange_diamond":"🔶","large_blue_diamond":"🔷","small_orange_diamond":"🔸","small_blue_diamond":"🔹","small_red_triangle":"🔺","small_red_triangle_down":"🔻","diamond_shape_with_a_dot_inside":"💠","radio_button":"🔘","white_square_button":"🔳","black_square_button":"🔲","checkered_flag":"🏁","triangular_flag_on_post":"🚩","crossed_flags":"🎌","black_flag":"🏴","white_flag":"🏳️","rainbow_flag":"🏳️‍🌈","transgender_flag":"🏳️‍⚧️","pirate_flag":"🏴‍☠️","ascension_island":"🇦🇨","andorra":"🇦🇩","united_arab_emirates":"🇦🇪","afghanistan":"🇦🇫","antigua_barbuda":"🇦🇬","anguilla":"🇦🇮","albania":"🇦🇱","armenia":"🇦🇲","angola":"🇦🇴","antarctica":"🇦🇶","argentina":"🇦🇷","american_samoa":"🇦🇸","austria":"🇦🇹","australia":"🇦🇺","aruba":"🇦🇼","aland_islands":"🇦🇽","azerbaijan":"🇦🇿","bosnia_herzegovina":"🇧🇦","barbados":"🇧🇧","bangladesh":"🇧🇩","belgium":"🇧🇪","burkina_faso":"🇧🇫","bulgaria":"🇧🇬","bahrain":"🇧🇭","burundi":"🇧🇮","benin":"🇧🇯","st_barthelemy":"🇧🇱","bermuda":"🇧🇲","brunei":"🇧🇳","bolivia":"🇧🇴","caribbean_netherlands":"🇧🇶","brazil":"🇧🇷","bahamas":"🇧🇸","bhutan":"🇧🇹","bouvet_island":"🇧🇻","botswana":"🇧🇼","belarus":"🇧🇾","belize":"🇧🇿","canada":"🇨🇦","cocos_islands":"🇨🇨","congo_kinshasa":"🇨🇩","central_african_republic":"🇨🇫","congo_brazzaville":"🇨🇬","switzerland":"🇨🇭","cote_divoire":"🇨🇮","cook_islands":"🇨🇰","chile":"🇨🇱","cameroon":"🇨🇲","cn":"🇨🇳","colombia":"🇨🇴","clipperton_island":"🇨🇵","costa_rica":"🇨🇷","cuba":"🇨🇺","cape_verde":"🇨🇻","curacao":"🇨🇼","christmas_island":"🇨🇽","cyprus":"🇨🇾","czech_republic":"🇨🇿","de":"🇩🇪","diego_garcia":"🇩🇬","djibouti":"🇩🇯","denmark":"🇩🇰","dominica":"🇩🇲","dominican_republic":"🇩🇴","algeria":"🇩🇿","ceuta_melilla":"🇪🇦","ecuador":"🇪🇨","estonia":"🇪🇪","egypt":"🇪🇬","western_sahara":"🇪🇭","eritrea":"🇪🇷","es":"🇪🇸","ethiopia":"🇪🇹","eu":"🇪🇺","european_union":"🇪🇺","finland":"🇫🇮","fiji":"🇫🇯","falkland_islands":"🇫🇰","micronesia":"🇫🇲","faroe_islands":"🇫🇴","fr":"🇫🇷","gabon":"🇬🇦","gb":"🇬🇧","uk":"🇬🇧","grenada":"🇬🇩","georgia":"🇬🇪","french_guiana":"🇬🇫","guernsey":"🇬🇬","ghana":"🇬🇭","gibraltar":"🇬🇮","greenland":"🇬🇱","gambia":"🇬🇲","guinea":"🇬🇳","guadeloupe":"🇬🇵","equatorial_guinea":"🇬🇶","greece":"🇬🇷","south_georgia_south_sandwich_islands":"🇬🇸","guatemala":"🇬🇹","guam":"🇬🇺","guinea_bissau":"🇬🇼","guyana":"🇬🇾","hong_kong":"🇭🇰","heard_mcdonald_islands":"🇭🇲","honduras":"🇭🇳","croatia":"🇭🇷","haiti":"🇭🇹","hungary":"🇭🇺","canary_islands":"🇮🇨","indonesia":"🇮🇩","ireland":"🇮🇪","israel":"🇮🇱","isle_of_man":"🇮🇲","india":"🇮🇳","british_indian_ocean_territory":"🇮🇴","iraq":"🇮🇶","iran":"🇮🇷","iceland":"🇮🇸","it":"🇮🇹","jersey":"🇯🇪","jamaica":"🇯🇲","jordan":"🇯🇴","jp":"🇯🇵","kenya":"🇰🇪","kyrgyzstan":"🇰🇬","cambodia":"🇰🇭","kiribati":"🇰🇮","comoros":"🇰🇲","st_kitts_nevis":"🇰🇳","north_korea":"🇰🇵","kr":"🇰🇷","kuwait":"🇰🇼","cayman_islands":"🇰🇾","kazakhstan":"🇰🇿","laos":"🇱🇦","lebanon":"🇱🇧","st_lucia":"🇱🇨","liechtenstein":"🇱🇮","sri_lanka":"🇱🇰","liberia":"🇱🇷","lesotho":"🇱🇸","lithuania":"🇱🇹","luxembourg":"🇱🇺","latvia":"🇱🇻","libya":"🇱🇾","morocco":"🇲🇦","monaco":"🇲🇨","moldova":"🇲🇩","montenegro":"🇲🇪","st_martin":"🇲🇫","madagascar":"🇲🇬","marshall_islands":"🇲🇭","macedonia":"🇲🇰","mali":"🇲🇱","myanmar":"🇲🇲","mongolia":"🇲🇳","macau":"🇲🇴","northern_mariana_islands":"🇲🇵","martinique":"🇲🇶","mauritania":"🇲🇷","montserrat":"🇲🇸","malta":"🇲🇹","mauritius":"🇲🇺","maldives":"🇲🇻","malawi":"🇲🇼","mexico":"🇲🇽","malaysia":"🇲🇾","mozambique":"🇲🇿","namibia":"🇳🇦","new_caledonia":"🇳🇨","niger":"🇳🇪","norfolk_island":"🇳🇫","nigeria":"🇳🇬","nicaragua":"🇳🇮","netherlands":"🇳🇱","norway":"🇳🇴","nepal":"🇳🇵","nauru":"🇳🇷","niue":"🇳🇺","new_zealand":"🇳🇿","oman":"🇴🇲","panama":"🇵🇦","peru":"🇵🇪","french_polynesia":"🇵🇫","papua_new_guinea":"🇵🇬","philippines":"🇵🇭","pakistan":"🇵🇰","poland":"🇵🇱","st_pierre_miquelon":"🇵🇲","pitcairn_islands":"🇵🇳","puerto_rico":"🇵🇷","palestinian_territories":"🇵🇸","portugal":"🇵🇹","palau":"🇵🇼","paraguay":"🇵🇾","qatar":"🇶🇦","reunion":"🇷🇪","romania":"🇷🇴","serbia":"🇷🇸","ru":"🇷🇺","rwanda":"🇷🇼","saudi_arabia":"🇸🇦","solomon_islands":"🇸🇧","seychelles":"🇸🇨","sudan":"🇸🇩","sweden":"🇸🇪","singapore":"🇸🇬","st_helena":"🇸🇭","slovenia":"🇸🇮","svalbard_jan_mayen":"🇸🇯","slovakia":"🇸🇰","sierra_leone":"🇸🇱","san_marino":"🇸🇲","senegal":"🇸🇳","somalia":"🇸🇴","suriname":"🇸🇷","south_sudan":"🇸🇸","sao_tome_principe":"🇸🇹","el_salvador":"🇸🇻","sint_maarten":"🇸🇽","syria":"🇸🇾","swaziland":"🇸🇿","tristan_da_cunha":"🇹🇦","turks_caicos_islands":"🇹🇨","chad":"🇹🇩","french_southern_territories":"🇹🇫","togo":"🇹🇬","thailand":"🇹🇭","tajikistan":"🇹🇯","tokelau":"🇹🇰","timor_leste":"🇹🇱","turkmenistan":"🇹🇲","tunisia":"🇹🇳","tonga":"🇹🇴","tr":"🇹🇷","trinidad_tobago":"🇹🇹","tuvalu":"🇹🇻","taiwan":"🇹🇼","tanzania":"🇹🇿","ukraine":"🇺🇦","uganda":"🇺🇬","us_outlying_islands":"🇺🇲","united_nations":"🇺🇳","us":"🇺🇸","uruguay":"🇺🇾","uzbekistan":"🇺🇿","vatican_city":"🇻🇦","st_vincent_grenadines":"🇻🇨","venezuela":"🇻🇪","british_virgin_islands":"🇻🇬","us_virgin_islands":"🇻🇮","vietnam":"🇻🇳","vanuatu":"🇻🇺","wallis_futuna":"🇼🇫","samoa":"🇼🇸","kosovo":"🇽🇰","yemen":"🇾🇪","mayotte":"🇾🇹","south_africa":"🇿🇦","zambia":"🇿🇲","zimbabwe":"🇿🇼","england":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","scotland":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","wales":"🏴󠁧󠁢󠁷󠁬󠁳󠁿"}
\ No newline at end of file
+export const emojiDictionary = {
+	"100": "💯",
+	"1234": "🔢",
+	grinning: "😀",
+	smiley: "😃",
+	smile: "😄",
+	grin: "😁",
+	laughing: "😆",
+	satisfied: "😆",
+	sweat_smile: "😅",
+	rofl: "🤣",
+	joy: "😂",
+	slightly_smiling_face: "🙂",
+	upside_down_face: "🙃",
+	wink: "😉",
+	blush: "😊",
+	innocent: "😇",
+	smiling_face_with_three_hearts: "🥰",
+	heart_eyes: "😍",
+	star_struck: "🤩",
+	kissing_heart: "😘",
+	kissing: "😗",
+	relaxed: "☺️",
+	kissing_closed_eyes: "😚",
+	kissing_smiling_eyes: "😙",
+	smiling_face_with_tear: "🥲",
+	yum: "😋",
+	stuck_out_tongue: "😛",
+	stuck_out_tongue_winking_eye: "😜",
+	zany_face: "🤪",
+	stuck_out_tongue_closed_eyes: "😝",
+	money_mouth_face: "🤑",
+	hugs: "🤗",
+	hand_over_mouth: "🤭",
+	shushing_face: "🤫",
+	thinking: "🤔",
+	zipper_mouth_face: "🤐",
+	raised_eyebrow: "🤨",
+	neutral_face: "😐",
+	expressionless: "😑",
+	no_mouth: "😶",
+	smirk: "😏",
+	unamused: "😒",
+	roll_eyes: "🙄",
+	grimacing: "😬",
+	lying_face: "🤥",
+	relieved: "😌",
+	pensive: "😔",
+	sleepy: "😪",
+	drooling_face: "🤤",
+	sleeping: "😴",
+	mask: "😷",
+	face_with_thermometer: "🤒",
+	face_with_head_bandage: "🤕",
+	nauseated_face: "🤢",
+	vomiting_face: "🤮",
+	sneezing_face: "🤧",
+	hot_face: "🥵",
+	cold_face: "🥶",
+	woozy_face: "🥴",
+	dizzy_face: "😵",
+	exploding_head: "🤯",
+	cowboy_hat_face: "🤠",
+	partying_face: "🥳",
+	disguised_face: "🥸",
+	sunglasses: "😎",
+	nerd_face: "🤓",
+	monocle_face: "🧐",
+	confused: "😕",
+	worried: "😟",
+	slightly_frowning_face: "🙁",
+	frowning_face: "☹️",
+	open_mouth: "😮",
+	hushed: "😯",
+	astonished: "😲",
+	flushed: "😳",
+	pleading_face: "🥺",
+	frowning: "😦",
+	anguished: "😧",
+	fearful: "😨",
+	cold_sweat: "😰",
+	disappointed_relieved: "😥",
+	cry: "😢",
+	sob: "😭",
+	scream: "😱",
+	confounded: "😖",
+	persevere: "😣",
+	disappointed: "😞",
+	sweat: "😓",
+	weary: "😩",
+	tired_face: "😫",
+	yawning_face: "🥱",
+	triumph: "😤",
+	rage: "😡",
+	pout: "😡",
+	angry: "😠",
+	cursing_face: "🤬",
+	smiling_imp: "😈",
+	imp: "👿",
+	skull: "💀",
+	skull_and_crossbones: "☠️",
+	hankey: "💩",
+	poop: "💩",
+	shit: "💩",
+	clown_face: "🤡",
+	japanese_ogre: "👹",
+	japanese_goblin: "👺",
+	ghost: "👻",
+	alien: "👽",
+	space_invader: "👾",
+	robot: "🤖",
+	smiley_cat: "😺",
+	smile_cat: "😸",
+	joy_cat: "😹",
+	heart_eyes_cat: "😻",
+	smirk_cat: "😼",
+	kissing_cat: "😽",
+	scream_cat: "🙀",
+	crying_cat_face: "😿",
+	pouting_cat: "😾",
+	see_no_evil: "🙈",
+	hear_no_evil: "🙉",
+	speak_no_evil: "🙊",
+	kiss: "💋",
+	love_letter: "💌",
+	cupid: "💘",
+	gift_heart: "💝",
+	sparkling_heart: "💖",
+	heartpulse: "💗",
+	heartbeat: "💓",
+	revolving_hearts: "💞",
+	two_hearts: "💕",
+	heart_decoration: "💟",
+	heavy_heart_exclamation: "❣️",
+	broken_heart: "💔",
+	heart: "❤️",
+	orange_heart: "🧡",
+	yellow_heart: "💛",
+	green_heart: "💚",
+	blue_heart: "💙",
+	purple_heart: "💜",
+	brown_heart: "🤎",
+	black_heart: "🖤",
+	white_heart: "🤍",
+	anger: "💢",
+	boom: "💥",
+	collision: "💥",
+	dizzy: "💫",
+	sweat_drops: "💦",
+	dash: "💨",
+	hole: "🕳️",
+	bomb: "💣",
+	speech_balloon: "💬",
+	eye_speech_bubble: "👁️‍🗨️",
+	left_speech_bubble: "🗨️",
+	right_anger_bubble: "🗯️",
+	thought_balloon: "💭",
+	zzz: "💤",
+	wave: "👋",
+	raised_back_of_hand: "🤚",
+	raised_hand_with_fingers_splayed: "🖐️",
+	hand: "✋",
+	raised_hand: "✋",
+	vulcan_salute: "🖖",
+	ok_hand: "👌",
+	pinched_fingers: "🤌",
+	pinching_hand: "🤏",
+	v: "✌️",
+	crossed_fingers: "🤞",
+	love_you_gesture: "🤟",
+	metal: "🤘",
+	call_me_hand: "🤙",
+	point_left: "👈",
+	point_right: "👉",
+	point_up_2: "👆",
+	middle_finger: "🖕",
+	fu: "🖕",
+	point_down: "👇",
+	point_up: "☝️",
+	"+1": "👍",
+	thumbsup: "👍",
+	"-1": "👎",
+	thumbsdown: "👎",
+	fist_raised: "✊",
+	fist: "✊",
+	fist_oncoming: "👊",
+	facepunch: "👊",
+	punch: "👊",
+	fist_left: "🤛",
+	fist_right: "🤜",
+	clap: "👏",
+	raised_hands: "🙌",
+	open_hands: "👐",
+	palms_up_together: "🤲",
+	handshake: "🤝",
+	pray: "🙏",
+	writing_hand: "✍️",
+	nail_care: "💅",
+	selfie: "🤳",
+	muscle: "💪",
+	mechanical_arm: "🦾",
+	mechanical_leg: "🦿",
+	leg: "🦵",
+	foot: "🦶",
+	ear: "👂",
+	ear_with_hearing_aid: "🦻",
+	nose: "👃",
+	brain: "🧠",
+	anatomical_heart: "🫀",
+	lungs: "🫁",
+	tooth: "🦷",
+	bone: "🦴",
+	eyes: "👀",
+	eye: "👁️",
+	tongue: "👅",
+	lips: "👄",
+	baby: "👶",
+	child: "🧒",
+	boy: "👦",
+	girl: "👧",
+	adult: "🧑",
+	blond_haired_person: "👱",
+	man: "👨",
+	bearded_person: "🧔",
+	red_haired_man: "👨‍🦰",
+	curly_haired_man: "👨‍🦱",
+	white_haired_man: "👨‍🦳",
+	bald_man: "👨‍🦲",
+	woman: "👩",
+	red_haired_woman: "👩‍🦰",
+	person_red_hair: "🧑‍🦰",
+	curly_haired_woman: "👩‍🦱",
+	person_curly_hair: "🧑‍🦱",
+	white_haired_woman: "👩‍🦳",
+	person_white_hair: "🧑‍🦳",
+	bald_woman: "👩‍🦲",
+	person_bald: "🧑‍🦲",
+	blond_haired_woman: "👱‍♀️",
+	blonde_woman: "👱‍♀️",
+	blond_haired_man: "👱‍♂️",
+	older_adult: "🧓",
+	older_man: "👴",
+	older_woman: "👵",
+	frowning_person: "🙍",
+	frowning_man: "🙍‍♂️",
+	frowning_woman: "🙍‍♀️",
+	pouting_face: "🙎",
+	pouting_man: "🙎‍♂️",
+	pouting_woman: "🙎‍♀️",
+	no_good: "🙅",
+	no_good_man: "🙅‍♂️",
+	ng_man: "🙅‍♂️",
+	no_good_woman: "🙅‍♀️",
+	ng_woman: "🙅‍♀️",
+	ok_person: "🙆",
+	ok_man: "🙆‍♂️",
+	ok_woman: "🙆‍♀️",
+	tipping_hand_person: "💁",
+	information_desk_person: "💁",
+	tipping_hand_man: "💁‍♂️",
+	sassy_man: "💁‍♂️",
+	tipping_hand_woman: "💁‍♀️",
+	sassy_woman: "💁‍♀️",
+	raising_hand: "🙋",
+	raising_hand_man: "🙋‍♂️",
+	raising_hand_woman: "🙋‍♀️",
+	deaf_person: "🧏",
+	deaf_man: "🧏‍♂️",
+	deaf_woman: "🧏‍♀️",
+	bow: "🙇",
+	bowing_man: "🙇‍♂️",
+	bowing_woman: "🙇‍♀️",
+	facepalm: "🤦",
+	man_facepalming: "🤦‍♂️",
+	woman_facepalming: "🤦‍♀️",
+	shrug: "🤷",
+	man_shrugging: "🤷‍♂️",
+	woman_shrugging: "🤷‍♀️",
+	health_worker: "🧑‍⚕️",
+	man_health_worker: "👨‍⚕️",
+	woman_health_worker: "👩‍⚕️",
+	student: "🧑‍🎓",
+	man_student: "👨‍🎓",
+	woman_student: "👩‍🎓",
+	teacher: "🧑‍🏫",
+	man_teacher: "👨‍🏫",
+	woman_teacher: "👩‍🏫",
+	judge: "🧑‍⚖️",
+	man_judge: "👨‍⚖️",
+	woman_judge: "👩‍⚖️",
+	farmer: "🧑‍🌾",
+	man_farmer: "👨‍🌾",
+	woman_farmer: "👩‍🌾",
+	cook: "🧑‍🍳",
+	man_cook: "👨‍🍳",
+	woman_cook: "👩‍🍳",
+	mechanic: "🧑‍🔧",
+	man_mechanic: "👨‍🔧",
+	woman_mechanic: "👩‍🔧",
+	factory_worker: "🧑‍🏭",
+	man_factory_worker: "👨‍🏭",
+	woman_factory_worker: "👩‍🏭",
+	office_worker: "🧑‍💼",
+	man_office_worker: "👨‍💼",
+	woman_office_worker: "👩‍💼",
+	scientist: "🧑‍🔬",
+	man_scientist: "👨‍🔬",
+	woman_scientist: "👩‍🔬",
+	technologist: "🧑‍💻",
+	man_technologist: "👨‍💻",
+	woman_technologist: "👩‍💻",
+	singer: "🧑‍🎤",
+	man_singer: "👨‍🎤",
+	woman_singer: "👩‍🎤",
+	artist: "🧑‍🎨",
+	man_artist: "👨‍🎨",
+	woman_artist: "👩‍🎨",
+	pilot: "🧑‍✈️",
+	man_pilot: "👨‍✈️",
+	woman_pilot: "👩‍✈️",
+	astronaut: "🧑‍🚀",
+	man_astronaut: "👨‍🚀",
+	woman_astronaut: "👩‍🚀",
+	firefighter: "🧑‍🚒",
+	man_firefighter: "👨‍🚒",
+	woman_firefighter: "👩‍🚒",
+	police_officer: "👮",
+	cop: "👮",
+	policeman: "👮‍♂️",
+	policewoman: "👮‍♀️",
+	detective: "🕵️",
+	male_detective: "🕵️‍♂️",
+	female_detective: "🕵️‍♀️",
+	guard: "💂",
+	guardsman: "💂‍♂️",
+	guardswoman: "💂‍♀️",
+	ninja: "🥷",
+	construction_worker: "👷",
+	construction_worker_man: "👷‍♂️",
+	construction_worker_woman: "👷‍♀️",
+	prince: "🤴",
+	princess: "👸",
+	person_with_turban: "👳",
+	man_with_turban: "👳‍♂️",
+	woman_with_turban: "👳‍♀️",
+	man_with_gua_pi_mao: "👲",
+	woman_with_headscarf: "🧕",
+	person_in_tuxedo: "🤵",
+	man_in_tuxedo: "🤵‍♂️",
+	woman_in_tuxedo: "🤵‍♀️",
+	person_with_veil: "👰",
+	man_with_veil: "👰‍♂️",
+	woman_with_veil: "👰‍♀️",
+	bride_with_veil: "👰‍♀️",
+	pregnant_woman: "🤰",
+	breast_feeding: "🤱",
+	woman_feeding_baby: "👩‍🍼",
+	man_feeding_baby: "👨‍🍼",
+	person_feeding_baby: "🧑‍🍼",
+	angel: "👼",
+	santa: "🎅",
+	mrs_claus: "🤶",
+	mx_claus: "🧑‍🎄",
+	superhero: "🦸",
+	superhero_man: "🦸‍♂️",
+	superhero_woman: "🦸‍♀️",
+	supervillain: "🦹",
+	supervillain_man: "🦹‍♂️",
+	supervillain_woman: "🦹‍♀️",
+	mage: "🧙",
+	mage_man: "🧙‍♂️",
+	mage_woman: "🧙‍♀️",
+	fairy: "🧚",
+	fairy_man: "🧚‍♂️",
+	fairy_woman: "🧚‍♀️",
+	vampire: "🧛",
+	vampire_man: "🧛‍♂️",
+	vampire_woman: "🧛‍♀️",
+	merperson: "🧜",
+	merman: "🧜‍♂️",
+	mermaid: "🧜‍♀️",
+	elf: "🧝",
+	elf_man: "🧝‍♂️",
+	elf_woman: "🧝‍♀️",
+	genie: "🧞",
+	genie_man: "🧞‍♂️",
+	genie_woman: "🧞‍♀️",
+	zombie: "🧟",
+	zombie_man: "🧟‍♂️",
+	zombie_woman: "🧟‍♀️",
+	massage: "💆",
+	massage_man: "💆‍♂️",
+	massage_woman: "💆‍♀️",
+	haircut: "💇",
+	haircut_man: "💇‍♂️",
+	haircut_woman: "💇‍♀️",
+	walking: "🚶",
+	walking_man: "🚶‍♂️",
+	walking_woman: "🚶‍♀️",
+	standing_person: "🧍",
+	standing_man: "🧍‍♂️",
+	standing_woman: "🧍‍♀️",
+	kneeling_person: "🧎",
+	kneeling_man: "🧎‍♂️",
+	kneeling_woman: "🧎‍♀️",
+	person_with_probing_cane: "🧑‍🦯",
+	man_with_probing_cane: "👨‍🦯",
+	woman_with_probing_cane: "👩‍🦯",
+	person_in_motorized_wheelchair: "🧑‍🦼",
+	man_in_motorized_wheelchair: "👨‍🦼",
+	woman_in_motorized_wheelchair: "👩‍🦼",
+	person_in_manual_wheelchair: "🧑‍🦽",
+	man_in_manual_wheelchair: "👨‍🦽",
+	woman_in_manual_wheelchair: "👩‍🦽",
+	runner: "🏃",
+	running: "🏃",
+	running_man: "🏃‍♂️",
+	running_woman: "🏃‍♀️",
+	woman_dancing: "💃",
+	dancer: "💃",
+	man_dancing: "🕺",
+	business_suit_levitating: "🕴️",
+	dancers: "👯",
+	dancing_men: "👯‍♂️",
+	dancing_women: "👯‍♀️",
+	sauna_person: "🧖",
+	sauna_man: "🧖‍♂️",
+	sauna_woman: "🧖‍♀️",
+	climbing: "🧗",
+	climbing_man: "🧗‍♂️",
+	climbing_woman: "🧗‍♀️",
+	person_fencing: "🤺",
+	horse_racing: "🏇",
+	skier: "⛷️",
+	snowboarder: "🏂",
+	golfing: "🏌️",
+	golfing_man: "🏌️‍♂️",
+	golfing_woman: "🏌️‍♀️",
+	surfer: "🏄",
+	surfing_man: "🏄‍♂️",
+	surfing_woman: "🏄‍♀️",
+	rowboat: "🚣",
+	rowing_man: "🚣‍♂️",
+	rowing_woman: "🚣‍♀️",
+	swimmer: "🏊",
+	swimming_man: "🏊‍♂️",
+	swimming_woman: "🏊‍♀️",
+	bouncing_ball_person: "⛹️",
+	bouncing_ball_man: "⛹️‍♂️",
+	basketball_man: "⛹️‍♂️",
+	bouncing_ball_woman: "⛹️‍♀️",
+	basketball_woman: "⛹️‍♀️",
+	weight_lifting: "🏋️",
+	weight_lifting_man: "🏋️‍♂️",
+	weight_lifting_woman: "🏋️‍♀️",
+	bicyclist: "🚴",
+	biking_man: "🚴‍♂️",
+	biking_woman: "🚴‍♀️",
+	mountain_bicyclist: "🚵",
+	mountain_biking_man: "🚵‍♂️",
+	mountain_biking_woman: "🚵‍♀️",
+	cartwheeling: "🤸",
+	man_cartwheeling: "🤸‍♂️",
+	woman_cartwheeling: "🤸‍♀️",
+	wrestling: "🤼",
+	men_wrestling: "🤼‍♂️",
+	women_wrestling: "🤼‍♀️",
+	water_polo: "🤽",
+	man_playing_water_polo: "🤽‍♂️",
+	woman_playing_water_polo: "🤽‍♀️",
+	handball_person: "🤾",
+	man_playing_handball: "🤾‍♂️",
+	woman_playing_handball: "🤾‍♀️",
+	juggling_person: "🤹",
+	man_juggling: "🤹‍♂️",
+	woman_juggling: "🤹‍♀️",
+	lotus_position: "🧘",
+	lotus_position_man: "🧘‍♂️",
+	lotus_position_woman: "🧘‍♀️",
+	bath: "🛀",
+	sleeping_bed: "🛌",
+	people_holding_hands: "🧑‍🤝‍🧑",
+	two_women_holding_hands: "👭",
+	couple: "👫",
+	two_men_holding_hands: "👬",
+	couplekiss: "💏",
+	couplekiss_man_woman: "👩‍❤️‍💋‍👨",
+	couplekiss_man_man: "👨‍❤️‍💋‍👨",
+	couplekiss_woman_woman: "👩‍❤️‍💋‍👩",
+	couple_with_heart: "💑",
+	couple_with_heart_woman_man: "👩‍❤️‍👨",
+	couple_with_heart_man_man: "👨‍❤️‍👨",
+	couple_with_heart_woman_woman: "👩‍❤️‍👩",
+	family: "👪",
+	family_man_woman_boy: "👨‍👩‍👦",
+	family_man_woman_girl: "👨‍👩‍👧",
+	family_man_woman_girl_boy: "👨‍👩‍👧‍👦",
+	family_man_woman_boy_boy: "👨‍👩‍👦‍👦",
+	family_man_woman_girl_girl: "👨‍👩‍👧‍👧",
+	family_man_man_boy: "👨‍👨‍👦",
+	family_man_man_girl: "👨‍👨‍👧",
+	family_man_man_girl_boy: "👨‍👨‍👧‍👦",
+	family_man_man_boy_boy: "👨‍👨‍👦‍👦",
+	family_man_man_girl_girl: "👨‍👨‍👧‍👧",
+	family_woman_woman_boy: "👩‍👩‍👦",
+	family_woman_woman_girl: "👩‍👩‍👧",
+	family_woman_woman_girl_boy: "👩‍👩‍👧‍👦",
+	family_woman_woman_boy_boy: "👩‍👩‍👦‍👦",
+	family_woman_woman_girl_girl: "👩‍👩‍👧‍👧",
+	family_man_boy: "👨‍👦",
+	family_man_boy_boy: "👨‍👦‍👦",
+	family_man_girl: "👨‍👧",
+	family_man_girl_boy: "👨‍👧‍👦",
+	family_man_girl_girl: "👨‍👧‍👧",
+	family_woman_boy: "👩‍👦",
+	family_woman_boy_boy: "👩‍👦‍👦",
+	family_woman_girl: "👩‍👧",
+	family_woman_girl_boy: "👩‍👧‍👦",
+	family_woman_girl_girl: "👩‍👧‍👧",
+	speaking_head: "🗣️",
+	bust_in_silhouette: "👤",
+	busts_in_silhouette: "👥",
+	people_hugging: "🫂",
+	footprints: "👣",
+	monkey_face: "🐵",
+	monkey: "🐒",
+	gorilla: "🦍",
+	orangutan: "🦧",
+	dog: "🐶",
+	dog2: "🐕",
+	guide_dog: "🦮",
+	service_dog: "🐕‍🦺",
+	poodle: "🐩",
+	wolf: "🐺",
+	fox_face: "🦊",
+	raccoon: "🦝",
+	cat: "🐱",
+	cat2: "🐈",
+	black_cat: "🐈‍⬛",
+	lion: "🦁",
+	tiger: "🐯",
+	tiger2: "🐅",
+	leopard: "🐆",
+	horse: "🐴",
+	racehorse: "🐎",
+	unicorn: "🦄",
+	zebra: "🦓",
+	deer: "🦌",
+	bison: "🦬",
+	cow: "🐮",
+	ox: "🐂",
+	water_buffalo: "🐃",
+	cow2: "🐄",
+	pig: "🐷",
+	pig2: "🐖",
+	boar: "🐗",
+	pig_nose: "🐽",
+	ram: "🐏",
+	sheep: "🐑",
+	goat: "🐐",
+	dromedary_camel: "🐪",
+	camel: "🐫",
+	llama: "🦙",
+	giraffe: "🦒",
+	elephant: "🐘",
+	mammoth: "🦣",
+	rhinoceros: "🦏",
+	hippopotamus: "🦛",
+	mouse: "🐭",
+	mouse2: "🐁",
+	rat: "🐀",
+	hamster: "🐹",
+	rabbit: "🐰",
+	rabbit2: "🐇",
+	chipmunk: "🐿️",
+	beaver: "🦫",
+	hedgehog: "🦔",
+	bat: "🦇",
+	bear: "🐻",
+	polar_bear: "🐻‍❄️",
+	koala: "🐨",
+	panda_face: "🐼",
+	sloth: "🦥",
+	otter: "🦦",
+	skunk: "🦨",
+	kangaroo: "🦘",
+	badger: "🦡",
+	feet: "🐾",
+	paw_prints: "🐾",
+	turkey: "🦃",
+	chicken: "🐔",
+	rooster: "🐓",
+	hatching_chick: "🐣",
+	baby_chick: "🐤",
+	hatched_chick: "🐥",
+	bird: "🐦",
+	penguin: "🐧",
+	dove: "🕊️",
+	eagle: "🦅",
+	duck: "🦆",
+	swan: "🦢",
+	owl: "🦉",
+	dodo: "🦤",
+	feather: "🪶",
+	flamingo: "🦩",
+	peacock: "🦚",
+	parrot: "🦜",
+	frog: "🐸",
+	crocodile: "🐊",
+	turtle: "🐢",
+	lizard: "🦎",
+	snake: "🐍",
+	dragon_face: "🐲",
+	dragon: "🐉",
+	sauropod: "🦕",
+	"t-rex": "🦖",
+	whale: "🐳",
+	whale2: "🐋",
+	dolphin: "🐬",
+	flipper: "🐬",
+	seal: "🦭",
+	fish: "🐟",
+	tropical_fish: "🐠",
+	blowfish: "🐡",
+	shark: "🦈",
+	octopus: "🐙",
+	shell: "🐚",
+	snail: "🐌",
+	butterfly: "🦋",
+	bug: "🐛",
+	ant: "🐜",
+	bee: "🐝",
+	honeybee: "🐝",
+	beetle: "🪲",
+	lady_beetle: "🐞",
+	cricket: "🦗",
+	cockroach: "🪳",
+	spider: "🕷️",
+	spider_web: "🕸️",
+	scorpion: "🦂",
+	mosquito: "🦟",
+	fly: "🪰",
+	worm: "🪱",
+	microbe: "🦠",
+	bouquet: "💐",
+	cherry_blossom: "🌸",
+	white_flower: "💮",
+	rosette: "🏵️",
+	rose: "🌹",
+	wilted_flower: "🥀",
+	hibiscus: "🌺",
+	sunflower: "🌻",
+	blossom: "🌼",
+	tulip: "🌷",
+	seedling: "🌱",
+	potted_plant: "🪴",
+	evergreen_tree: "🌲",
+	deciduous_tree: "🌳",
+	palm_tree: "🌴",
+	cactus: "🌵",
+	ear_of_rice: "🌾",
+	herb: "🌿",
+	shamrock: "☘️",
+	four_leaf_clover: "🍀",
+	maple_leaf: "🍁",
+	fallen_leaf: "🍂",
+	leaves: "🍃",
+	grapes: "🍇",
+	melon: "🍈",
+	watermelon: "🍉",
+	tangerine: "🍊",
+	orange: "🍊",
+	mandarin: "🍊",
+	lemon: "🍋",
+	banana: "🍌",
+	pineapple: "🍍",
+	mango: "🥭",
+	apple: "🍎",
+	green_apple: "🍏",
+	pear: "🍐",
+	peach: "🍑",
+	cherries: "🍒",
+	strawberry: "🍓",
+	blueberries: "🫐",
+	kiwi_fruit: "🥝",
+	tomato: "🍅",
+	olive: "🫒",
+	coconut: "🥥",
+	avocado: "🥑",
+	eggplant: "🍆",
+	potato: "🥔",
+	carrot: "🥕",
+	corn: "🌽",
+	hot_pepper: "🌶️",
+	bell_pepper: "🫑",
+	cucumber: "🥒",
+	leafy_green: "🥬",
+	broccoli: "🥦",
+	garlic: "🧄",
+	onion: "🧅",
+	mushroom: "🍄",
+	peanuts: "🥜",
+	chestnut: "🌰",
+	bread: "🍞",
+	croissant: "🥐",
+	baguette_bread: "🥖",
+	flatbread: "🫓",
+	pretzel: "🥨",
+	bagel: "🥯",
+	pancakes: "🥞",
+	waffle: "🧇",
+	cheese: "🧀",
+	meat_on_bone: "🍖",
+	poultry_leg: "🍗",
+	cut_of_meat: "🥩",
+	bacon: "🥓",
+	hamburger: "🍔",
+	fries: "🍟",
+	pizza: "🍕",
+	hotdog: "🌭",
+	sandwich: "🥪",
+	taco: "🌮",
+	burrito: "🌯",
+	tamale: "🫔",
+	stuffed_flatbread: "🥙",
+	falafel: "🧆",
+	egg: "🥚",
+	fried_egg: "🍳",
+	shallow_pan_of_food: "🥘",
+	stew: "🍲",
+	fondue: "🫕",
+	bowl_with_spoon: "🥣",
+	green_salad: "🥗",
+	popcorn: "🍿",
+	butter: "🧈",
+	salt: "🧂",
+	canned_food: "🥫",
+	bento: "🍱",
+	rice_cracker: "🍘",
+	rice_ball: "🍙",
+	rice: "🍚",
+	curry: "🍛",
+	ramen: "🍜",
+	spaghetti: "🍝",
+	sweet_potato: "🍠",
+	oden: "🍢",
+	sushi: "🍣",
+	fried_shrimp: "🍤",
+	fish_cake: "🍥",
+	moon_cake: "🥮",
+	dango: "🍡",
+	dumpling: "🥟",
+	fortune_cookie: "🥠",
+	takeout_box: "🥡",
+	crab: "🦀",
+	lobster: "🦞",
+	shrimp: "🦐",
+	squid: "🦑",
+	oyster: "🦪",
+	icecream: "🍦",
+	shaved_ice: "🍧",
+	ice_cream: "🍨",
+	doughnut: "🍩",
+	cookie: "🍪",
+	birthday: "🎂",
+	cake: "🍰",
+	cupcake: "🧁",
+	pie: "🥧",
+	chocolate_bar: "🍫",
+	candy: "🍬",
+	lollipop: "🍭",
+	custard: "🍮",
+	honey_pot: "🍯",
+	baby_bottle: "🍼",
+	milk_glass: "🥛",
+	coffee: "☕",
+	teapot: "🫖",
+	tea: "🍵",
+	sake: "🍶",
+	champagne: "🍾",
+	wine_glass: "🍷",
+	cocktail: "🍸",
+	tropical_drink: "🍹",
+	beer: "🍺",
+	beers: "🍻",
+	clinking_glasses: "🥂",
+	tumbler_glass: "🥃",
+	cup_with_straw: "🥤",
+	bubble_tea: "🧋",
+	beverage_box: "🧃",
+	mate: "🧉",
+	ice_cube: "🧊",
+	chopsticks: "🥢",
+	plate_with_cutlery: "🍽️",
+	fork_and_knife: "🍴",
+	spoon: "🥄",
+	hocho: "🔪",
+	knife: "🔪",
+	amphora: "🏺",
+	earth_africa: "🌍",
+	earth_americas: "🌎",
+	earth_asia: "🌏",
+	globe_with_meridians: "🌐",
+	world_map: "🗺️",
+	japan: "🗾",
+	compass: "🧭",
+	mountain_snow: "🏔️",
+	mountain: "⛰️",
+	volcano: "🌋",
+	mount_fuji: "🗻",
+	camping: "🏕️",
+	beach_umbrella: "🏖️",
+	desert: "🏜️",
+	desert_island: "🏝️",
+	national_park: "🏞️",
+	stadium: "🏟️",
+	classical_building: "🏛️",
+	building_construction: "🏗️",
+	bricks: "🧱",
+	rock: "🪨",
+	wood: "🪵",
+	hut: "🛖",
+	houses: "🏘️",
+	derelict_house: "🏚️",
+	house: "🏠",
+	house_with_garden: "🏡",
+	office: "🏢",
+	post_office: "🏣",
+	european_post_office: "🏤",
+	hospital: "🏥",
+	bank: "🏦",
+	hotel: "🏨",
+	love_hotel: "🏩",
+	convenience_store: "🏪",
+	school: "🏫",
+	department_store: "🏬",
+	factory: "🏭",
+	japanese_castle: "🏯",
+	european_castle: "🏰",
+	wedding: "💒",
+	tokyo_tower: "🗼",
+	statue_of_liberty: "🗽",
+	church: "⛪",
+	mosque: "🕌",
+	hindu_temple: "🛕",
+	synagogue: "🕍",
+	shinto_shrine: "⛩️",
+	kaaba: "🕋",
+	fountain: "⛲",
+	tent: "⛺",
+	foggy: "🌁",
+	night_with_stars: "🌃",
+	cityscape: "🏙️",
+	sunrise_over_mountains: "🌄",
+	sunrise: "🌅",
+	city_sunset: "🌆",
+	city_sunrise: "🌇",
+	bridge_at_night: "🌉",
+	hotsprings: "♨️",
+	carousel_horse: "🎠",
+	ferris_wheel: "🎡",
+	roller_coaster: "🎢",
+	barber: "💈",
+	circus_tent: "🎪",
+	steam_locomotive: "🚂",
+	railway_car: "🚃",
+	bullettrain_side: "🚄",
+	bullettrain_front: "🚅",
+	train2: "🚆",
+	metro: "🚇",
+	light_rail: "🚈",
+	station: "🚉",
+	tram: "🚊",
+	monorail: "🚝",
+	mountain_railway: "🚞",
+	train: "🚋",
+	bus: "🚌",
+	oncoming_bus: "🚍",
+	trolleybus: "🚎",
+	minibus: "🚐",
+	ambulance: "🚑",
+	fire_engine: "🚒",
+	police_car: "🚓",
+	oncoming_police_car: "🚔",
+	taxi: "🚕",
+	oncoming_taxi: "🚖",
+	car: "🚗",
+	red_car: "🚗",
+	oncoming_automobile: "🚘",
+	blue_car: "🚙",
+	pickup_truck: "🛻",
+	truck: "🚚",
+	articulated_lorry: "🚛",
+	tractor: "🚜",
+	racing_car: "🏎️",
+	motorcycle: "🏍️",
+	motor_scooter: "🛵",
+	manual_wheelchair: "🦽",
+	motorized_wheelchair: "🦼",
+	auto_rickshaw: "🛺",
+	bike: "🚲",
+	kick_scooter: "🛴",
+	skateboard: "🛹",
+	roller_skate: "🛼",
+	busstop: "🚏",
+	motorway: "🛣️",
+	railway_track: "🛤️",
+	oil_drum: "🛢️",
+	fuelpump: "⛽",
+	rotating_light: "🚨",
+	traffic_light: "🚥",
+	vertical_traffic_light: "🚦",
+	stop_sign: "🛑",
+	construction: "🚧",
+	anchor: "âš“",
+	boat: "⛵",
+	sailboat: "⛵",
+	canoe: "🛶",
+	speedboat: "🚤",
+	passenger_ship: "🛳️",
+	ferry: "⛴️",
+	motor_boat: "🛥️",
+	ship: "🚢",
+	airplane: "✈️",
+	small_airplane: "🛩️",
+	flight_departure: "🛫",
+	flight_arrival: "🛬",
+	parachute: "🪂",
+	seat: "💺",
+	helicopter: "🚁",
+	suspension_railway: "🚟",
+	mountain_cableway: "🚠",
+	aerial_tramway: "🚡",
+	artificial_satellite: "🛰️",
+	rocket: "🚀",
+	flying_saucer: "🛸",
+	bellhop_bell: "🛎️",
+	luggage: "🧳",
+	hourglass: "⌛",
+	hourglass_flowing_sand: "⏳",
+	watch: "⌚",
+	alarm_clock: "⏰",
+	stopwatch: "⏱️",
+	timer_clock: "⏲️",
+	mantelpiece_clock: "🕰️",
+	clock12: "🕛",
+	clock1230: "🕧",
+	clock1: "🕐",
+	clock130: "🕜",
+	clock2: "🕑",
+	clock230: "🕝",
+	clock3: "🕒",
+	clock330: "🕞",
+	clock4: "🕓",
+	clock430: "🕟",
+	clock5: "🕔",
+	clock530: "🕠",
+	clock6: "🕕",
+	clock630: "🕡",
+	clock7: "🕖",
+	clock730: "🕢",
+	clock8: "🕗",
+	clock830: "🕣",
+	clock9: "🕘",
+	clock930: "🕤",
+	clock10: "🕙",
+	clock1030: "🕥",
+	clock11: "🕚",
+	clock1130: "🕦",
+	new_moon: "🌑",
+	waxing_crescent_moon: "🌒",
+	first_quarter_moon: "🌓",
+	moon: "🌔",
+	waxing_gibbous_moon: "🌔",
+	full_moon: "🌕",
+	waning_gibbous_moon: "🌖",
+	last_quarter_moon: "🌗",
+	waning_crescent_moon: "🌘",
+	crescent_moon: "🌙",
+	new_moon_with_face: "🌚",
+	first_quarter_moon_with_face: "🌛",
+	last_quarter_moon_with_face: "🌜",
+	thermometer: "🌡️",
+	sunny: "☀️",
+	full_moon_with_face: "🌝",
+	sun_with_face: "🌞",
+	ringed_planet: "🪐",
+	star: "⭐",
+	star2: "🌟",
+	stars: "🌠",
+	milky_way: "🌌",
+	cloud: "☁️",
+	partly_sunny: "â›…",
+	cloud_with_lightning_and_rain: "⛈️",
+	sun_behind_small_cloud: "🌤️",
+	sun_behind_large_cloud: "🌥️",
+	sun_behind_rain_cloud: "🌦️",
+	cloud_with_rain: "🌧️",
+	cloud_with_snow: "🌨️",
+	cloud_with_lightning: "🌩️",
+	tornado: "🌪️",
+	fog: "🌫️",
+	wind_face: "🌬️",
+	cyclone: "🌀",
+	rainbow: "🌈",
+	closed_umbrella: "🌂",
+	open_umbrella: "☂️",
+	umbrella: "☔",
+	parasol_on_ground: "⛱️",
+	zap: "âš¡",
+	snowflake: "❄️",
+	snowman_with_snow: "☃️",
+	snowman: "⛄",
+	comet: "☄️",
+	fire: "🔥",
+	droplet: "💧",
+	ocean: "🌊",
+	jack_o_lantern: "🎃",
+	christmas_tree: "🎄",
+	fireworks: "🎆",
+	sparkler: "🎇",
+	firecracker: "🧨",
+	sparkles: "✨",
+	balloon: "🎈",
+	tada: "🎉",
+	confetti_ball: "🎊",
+	tanabata_tree: "🎋",
+	bamboo: "🎍",
+	dolls: "🎎",
+	flags: "🎏",
+	wind_chime: "🎐",
+	rice_scene: "🎑",
+	red_envelope: "🧧",
+	ribbon: "🎀",
+	gift: "🎁",
+	reminder_ribbon: "🎗️",
+	tickets: "🎟️",
+	ticket: "🎫",
+	medal_military: "🎖️",
+	trophy: "🏆",
+	medal_sports: "🏅",
+	"1st_place_medal": "🥇",
+	"2nd_place_medal": "🥈",
+	"3rd_place_medal": "🥉",
+	soccer: "âš½",
+	baseball: "âš¾",
+	softball: "🥎",
+	basketball: "🏀",
+	volleyball: "🏐",
+	football: "🏈",
+	rugby_football: "🏉",
+	tennis: "🎾",
+	flying_disc: "🥏",
+	bowling: "🎳",
+	cricket_game: "🏏",
+	field_hockey: "🏑",
+	ice_hockey: "🏒",
+	lacrosse: "🥍",
+	ping_pong: "🏓",
+	badminton: "🏸",
+	boxing_glove: "🥊",
+	martial_arts_uniform: "🥋",
+	goal_net: "🥅",
+	golf: "⛳",
+	ice_skate: "⛸️",
+	fishing_pole_and_fish: "🎣",
+	diving_mask: "🤿",
+	running_shirt_with_sash: "🎽",
+	ski: "🎿",
+	sled: "🛷",
+	curling_stone: "🥌",
+	dart: "🎯",
+	yo_yo: "🪀",
+	kite: "🪁",
+	"8ball": "🎱",
+	crystal_ball: "🔮",
+	magic_wand: "🪄",
+	nazar_amulet: "🧿",
+	video_game: "🎮",
+	joystick: "🕹️",
+	slot_machine: "🎰",
+	game_die: "🎲",
+	jigsaw: "🧩",
+	teddy_bear: "🧸",
+	pinata: "🪅",
+	nesting_dolls: "🪆",
+	spades: "♠️",
+	hearts: "♥️",
+	diamonds: "♦️",
+	clubs: "♣️",
+	chess_pawn: "♟️",
+	black_joker: "🃏",
+	mahjong: "🀄",
+	flower_playing_cards: "🎴",
+	performing_arts: "🎭",
+	framed_picture: "🖼️",
+	art: "🎨",
+	thread: "🧵",
+	sewing_needle: "🪡",
+	yarn: "🧶",
+	knot: "🪢",
+	eyeglasses: "👓",
+	dark_sunglasses: "🕶️",
+	goggles: "🥽",
+	lab_coat: "🥼",
+	safety_vest: "🦺",
+	necktie: "👔",
+	shirt: "👕",
+	tshirt: "👕",
+	jeans: "👖",
+	scarf: "🧣",
+	gloves: "🧤",
+	coat: "🧥",
+	socks: "🧦",
+	dress: "👗",
+	kimono: "👘",
+	sari: "🥻",
+	one_piece_swimsuit: "🩱",
+	swim_brief: "🩲",
+	shorts: "🩳",
+	bikini: "👙",
+	womans_clothes: "👚",
+	purse: "👛",
+	handbag: "👜",
+	pouch: "👝",
+	shopping: "🛍️",
+	school_satchel: "🎒",
+	thong_sandal: "🩴",
+	mans_shoe: "👞",
+	shoe: "👞",
+	athletic_shoe: "👟",
+	hiking_boot: "🥾",
+	flat_shoe: "🥿",
+	high_heel: "👠",
+	sandal: "👡",
+	ballet_shoes: "🩰",
+	boot: "👢",
+	crown: "👑",
+	womans_hat: "👒",
+	tophat: "🎩",
+	mortar_board: "🎓",
+	billed_cap: "🧢",
+	military_helmet: "🪖",
+	rescue_worker_helmet: "⛑️",
+	prayer_beads: "📿",
+	lipstick: "💄",
+	ring: "💍",
+	gem: "💎",
+	mute: "🔇",
+	speaker: "🔈",
+	sound: "🔉",
+	loud_sound: "🔊",
+	loudspeaker: "📢",
+	mega: "📣",
+	postal_horn: "📯",
+	bell: "🔔",
+	no_bell: "🔕",
+	musical_score: "🎼",
+	musical_note: "🎵",
+	notes: "🎶",
+	studio_microphone: "🎙️",
+	level_slider: "🎚️",
+	control_knobs: "🎛️",
+	microphone: "🎤",
+	headphones: "🎧",
+	radio: "📻",
+	saxophone: "🎷",
+	accordion: "🪗",
+	guitar: "🎸",
+	musical_keyboard: "🎹",
+	trumpet: "🎺",
+	violin: "🎻",
+	banjo: "🪕",
+	drum: "🥁",
+	long_drum: "🪘",
+	iphone: "📱",
+	calling: "📲",
+	phone: "☎️",
+	telephone: "☎️",
+	telephone_receiver: "📞",
+	pager: "📟",
+	fax: "📠",
+	battery: "🔋",
+	electric_plug: "🔌",
+	computer: "💻",
+	desktop_computer: "🖥️",
+	printer: "🖨️",
+	keyboard: "⌨️",
+	computer_mouse: "🖱️",
+	trackball: "🖲️",
+	minidisc: "💽",
+	floppy_disk: "💾",
+	cd: "💿",
+	dvd: "📀",
+	abacus: "🧮",
+	movie_camera: "🎥",
+	film_strip: "🎞️",
+	film_projector: "📽️",
+	clapper: "🎬",
+	tv: "📺",
+	camera: "📷",
+	camera_flash: "📸",
+	video_camera: "📹",
+	vhs: "📼",
+	mag: "🔍",
+	mag_right: "🔎",
+	candle: "🕯️",
+	bulb: "💡",
+	flashlight: "🔦",
+	izakaya_lantern: "🏮",
+	lantern: "🏮",
+	diya_lamp: "🪔",
+	notebook_with_decorative_cover: "📔",
+	closed_book: "📕",
+	book: "📖",
+	open_book: "📖",
+	green_book: "📗",
+	blue_book: "📘",
+	orange_book: "📙",
+	books: "📚",
+	notebook: "📓",
+	ledger: "📒",
+	page_with_curl: "📃",
+	scroll: "📜",
+	page_facing_up: "📄",
+	newspaper: "📰",
+	newspaper_roll: "🗞️",
+	bookmark_tabs: "📑",
+	bookmark: "🔖",
+	label: "🏷️",
+	moneybag: "💰",
+	coin: "🪙",
+	yen: "💴",
+	dollar: "💵",
+	euro: "💶",
+	pound: "💷",
+	money_with_wings: "💸",
+	credit_card: "💳",
+	receipt: "🧾",
+	chart: "💹",
+	envelope: "✉️",
+	email: "📧",
+	"e-mail": "📧",
+	incoming_envelope: "📨",
+	envelope_with_arrow: "📩",
+	outbox_tray: "📤",
+	inbox_tray: "📥",
+	package: "📦",
+	mailbox: "📫",
+	mailbox_closed: "📪",
+	mailbox_with_mail: "📬",
+	mailbox_with_no_mail: "📭",
+	postbox: "📮",
+	ballot_box: "🗳️",
+	pencil2: "✏️",
+	black_nib: "✒️",
+	fountain_pen: "🖋️",
+	pen: "🖊️",
+	paintbrush: "🖌️",
+	crayon: "🖍️",
+	memo: "📝",
+	pencil: "📝",
+	briefcase: "💼",
+	file_folder: "📁",
+	open_file_folder: "📂",
+	card_index_dividers: "🗂️",
+	date: "📅",
+	calendar: "📆",
+	spiral_notepad: "🗒️",
+	spiral_calendar: "🗓️",
+	card_index: "📇",
+	chart_with_upwards_trend: "📈",
+	chart_with_downwards_trend: "📉",
+	bar_chart: "📊",
+	clipboard: "📋",
+	pushpin: "📌",
+	round_pushpin: "📍",
+	paperclip: "📎",
+	paperclips: "🖇️",
+	straight_ruler: "📏",
+	triangular_ruler: "📐",
+	scissors: "✂️",
+	card_file_box: "🗃️",
+	file_cabinet: "🗄️",
+	wastebasket: "🗑️",
+	lock: "🔒",
+	unlock: "🔓",
+	lock_with_ink_pen: "🔏",
+	closed_lock_with_key: "🔐",
+	key: "🔑",
+	old_key: "🗝️",
+	hammer: "🔨",
+	axe: "🪓",
+	pick: "⛏️",
+	hammer_and_pick: "⚒️",
+	hammer_and_wrench: "🛠️",
+	dagger: "🗡️",
+	crossed_swords: "⚔️",
+	gun: "🔫",
+	boomerang: "🪃",
+	bow_and_arrow: "🏹",
+	shield: "🛡️",
+	carpentry_saw: "🪚",
+	wrench: "🔧",
+	screwdriver: "🪛",
+	nut_and_bolt: "🔩",
+	gear: "⚙️",
+	clamp: "🗜️",
+	balance_scale: "⚖️",
+	probing_cane: "🦯",
+	link: "🔗",
+	chains: "⛓️",
+	hook: "🪝",
+	toolbox: "🧰",
+	magnet: "🧲",
+	ladder: "🪜",
+	alembic: "⚗️",
+	test_tube: "🧪",
+	petri_dish: "🧫",
+	dna: "🧬",
+	microscope: "🔬",
+	telescope: "🔭",
+	satellite: "📡",
+	syringe: "💉",
+	drop_of_blood: "🩸",
+	pill: "💊",
+	adhesive_bandage: "🩹",
+	stethoscope: "🩺",
+	door: "🚪",
+	elevator: "🛗",
+	mirror: "🪞",
+	window: "🪟",
+	bed: "🛏️",
+	couch_and_lamp: "🛋️",
+	chair: "🪑",
+	toilet: "🚽",
+	plunger: "🪠",
+	shower: "🚿",
+	bathtub: "🛁",
+	mouse_trap: "🪤",
+	razor: "🪒",
+	lotion_bottle: "🧴",
+	safety_pin: "🧷",
+	broom: "🧹",
+	basket: "🧺",
+	roll_of_paper: "🧻",
+	bucket: "🪣",
+	soap: "🧼",
+	toothbrush: "🪥",
+	sponge: "🧽",
+	fire_extinguisher: "🧯",
+	shopping_cart: "🛒",
+	smoking: "🚬",
+	coffin: "⚰️",
+	headstone: "🪦",
+	funeral_urn: "⚱️",
+	moyai: "🗿",
+	placard: "🪧",
+	atm: "🏧",
+	put_litter_in_its_place: "🚮",
+	potable_water: "🚰",
+	wheelchair: "♿",
+	mens: "🚹",
+	womens: "🚺",
+	restroom: "🚻",
+	baby_symbol: "🚼",
+	wc: "🚾",
+	passport_control: "🛂",
+	customs: "🛃",
+	baggage_claim: "🛄",
+	left_luggage: "🛅",
+	warning: "⚠️",
+	children_crossing: "🚸",
+	no_entry: "â›”",
+	no_entry_sign: "🚫",
+	no_bicycles: "🚳",
+	no_smoking: "🚭",
+	do_not_litter: "🚯",
+	"non-potable_water": "🚱",
+	no_pedestrians: "🚷",
+	no_mobile_phones: "📵",
+	underage: "🔞",
+	radioactive: "☢️",
+	biohazard: "☣️",
+	arrow_up: "⬆️",
+	arrow_upper_right: "↗️",
+	arrow_right: "➡️",
+	arrow_lower_right: "↘️",
+	arrow_down: "⬇️",
+	arrow_lower_left: "↙️",
+	arrow_left: "⬅️",
+	arrow_upper_left: "↖️",
+	arrow_up_down: "↕️",
+	left_right_arrow: "↔️",
+	leftwards_arrow_with_hook: "↩️",
+	arrow_right_hook: "↪️",
+	arrow_heading_up: "⤴️",
+	arrow_heading_down: "⤵️",
+	arrows_clockwise: "🔃",
+	arrows_counterclockwise: "🔄",
+	back: "🔙",
+	end: "🔚",
+	on: "🔛",
+	soon: "🔜",
+	top: "🔝",
+	place_of_worship: "🛐",
+	atom_symbol: "⚛️",
+	om: "🕉️",
+	star_of_david: "✡️",
+	wheel_of_dharma: "☸️",
+	yin_yang: "☯️",
+	latin_cross: "✝️",
+	orthodox_cross: "☦️",
+	star_and_crescent: "☪️",
+	peace_symbol: "☮️",
+	menorah: "🕎",
+	six_pointed_star: "🔯",
+	aries: "♈",
+	taurus: "♉",
+	gemini: "♊",
+	cancer: "♋",
+	leo: "♌",
+	virgo: "♍",
+	libra: "♎",
+	scorpius: "♏",
+	sagittarius: "♐",
+	capricorn: "♑",
+	aquarius: "â™’",
+	pisces: "♓",
+	ophiuchus: "⛎",
+	twisted_rightwards_arrows: "🔀",
+	repeat: "🔁",
+	repeat_one: "🔂",
+	arrow_forward: "▶️",
+	fast_forward: "⏩",
+	next_track_button: "⏭️",
+	play_or_pause_button: "⏯️",
+	arrow_backward: "◀️",
+	rewind: "⏪",
+	previous_track_button: "⏮️",
+	arrow_up_small: "🔼",
+	arrow_double_up: "⏫",
+	arrow_down_small: "🔽",
+	arrow_double_down: "⏬",
+	pause_button: "⏸️",
+	stop_button: "⏹️",
+	record_button: "⏺️",
+	eject_button: "⏏️",
+	cinema: "🎦",
+	low_brightness: "🔅",
+	high_brightness: "🔆",
+	signal_strength: "📶",
+	vibration_mode: "📳",
+	mobile_phone_off: "📴",
+	female_sign: "♀️",
+	male_sign: "♂️",
+	transgender_symbol: "⚧️",
+	heavy_multiplication_x: "✖️",
+	heavy_plus_sign: "âž•",
+	heavy_minus_sign: "âž–",
+	heavy_division_sign: "âž—",
+	infinity: "♾️",
+	bangbang: "‼️",
+	interrobang: "⁉️",
+	question: "❓",
+	grey_question: "❔",
+	grey_exclamation: "❕",
+	exclamation: "❗",
+	heavy_exclamation_mark: "❗",
+	wavy_dash: "〰️",
+	currency_exchange: "💱",
+	heavy_dollar_sign: "💲",
+	medical_symbol: "⚕️",
+	recycle: "♻️",
+	fleur_de_lis: "⚜️",
+	trident: "🔱",
+	name_badge: "📛",
+	beginner: "🔰",
+	o: "â­•",
+	white_check_mark: "✅",
+	ballot_box_with_check: "☑️",
+	heavy_check_mark: "✔️",
+	x: "❌",
+	negative_squared_cross_mark: "❎",
+	curly_loop: "âž°",
+	loop: "âž¿",
+	part_alternation_mark: "〽️",
+	eight_spoked_asterisk: "✳️",
+	eight_pointed_black_star: "✴️",
+	sparkle: "❇️",
+	copyright: "©️",
+	registered: "®️",
+	tm: "™️",
+	hash: "#️⃣",
+	asterisk: "*️⃣",
+	zero: "0️⃣",
+	one: "1️⃣",
+	two: "2️⃣",
+	three: "3️⃣",
+	four: "4️⃣",
+	five: "5️⃣",
+	six: "6️⃣",
+	seven: "7️⃣",
+	eight: "8️⃣",
+	nine: "9️⃣",
+	keycap_ten: "🔟",
+	capital_abcd: "🔠",
+	abcd: "🔡",
+	symbols: "🔣",
+	abc: "🔤",
+	a: "🅰️",
+	ab: "🆎",
+	b: "🅱️",
+	cl: "🆑",
+	cool: "🆒",
+	free: "🆓",
+	information_source: "ℹ️",
+	id: "🆔",
+	m: "Ⓜ️",
+	new: "🆕",
+	ng: "🆖",
+	o2: "🅾️",
+	ok: "🆗",
+	parking: "🅿️",
+	sos: "🆘",
+	up: "🆙",
+	vs: "🆚",
+	koko: "🈁",
+	sa: "🈂️",
+	u6708: "🈷️",
+	u6709: "🈶",
+	u6307: "🈯",
+	ideograph_advantage: "🉐",
+	u5272: "🈹",
+	u7121: "🈚",
+	u7981: "🈲",
+	accept: "🉑",
+	u7533: "🈸",
+	u5408: "🈴",
+	u7a7a: "🈳",
+	congratulations: "㊗️",
+	secret: "㊙️",
+	u55b6: "🈺",
+	u6e80: "🈵",
+	red_circle: "🔴",
+	orange_circle: "🟠",
+	yellow_circle: "🟡",
+	green_circle: "🟢",
+	large_blue_circle: "🔵",
+	purple_circle: "🟣",
+	brown_circle: "🟤",
+	black_circle: "âš«",
+	white_circle: "⚪",
+	red_square: "🟥",
+	orange_square: "🟧",
+	yellow_square: "🟨",
+	green_square: "🟩",
+	blue_square: "🟦",
+	purple_square: "🟪",
+	brown_square: "🟫",
+	black_large_square: "⬛",
+	white_large_square: "⬜",
+	black_medium_square: "◼️",
+	white_medium_square: "◻️",
+	black_medium_small_square: "â—¾",
+	white_medium_small_square: "â—½",
+	black_small_square: "▪️",
+	white_small_square: "▫️",
+	large_orange_diamond: "🔶",
+	large_blue_diamond: "🔷",
+	small_orange_diamond: "🔸",
+	small_blue_diamond: "🔹",
+	small_red_triangle: "🔺",
+	small_red_triangle_down: "🔻",
+	diamond_shape_with_a_dot_inside: "💠",
+	radio_button: "🔘",
+	white_square_button: "🔳",
+	black_square_button: "🔲",
+	checkered_flag: "🏁",
+	triangular_flag_on_post: "🚩",
+	crossed_flags: "🎌",
+	black_flag: "🏴",
+	white_flag: "🏳️",
+	rainbow_flag: "🏳️‍🌈",
+	transgender_flag: "🏳️‍⚧️",
+	pirate_flag: "🏴‍☠️",
+	ascension_island: "🇦🇨",
+	andorra: "🇦🇩",
+	united_arab_emirates: "🇦🇪",
+	afghanistan: "🇦🇫",
+	antigua_barbuda: "🇦🇬",
+	anguilla: "🇦🇮",
+	albania: "🇦🇱",
+	armenia: "🇦🇲",
+	angola: "🇦🇴",
+	antarctica: "🇦🇶",
+	argentina: "🇦🇷",
+	american_samoa: "🇦🇸",
+	austria: "🇦🇹",
+	australia: "🇦🇺",
+	aruba: "🇦🇼",
+	aland_islands: "🇦🇽",
+	azerbaijan: "🇦🇿",
+	bosnia_herzegovina: "🇧🇦",
+	barbados: "🇧🇧",
+	bangladesh: "🇧🇩",
+	belgium: "🇧🇪",
+	burkina_faso: "🇧🇫",
+	bulgaria: "🇧🇬",
+	bahrain: "🇧🇭",
+	burundi: "🇧🇮",
+	benin: "🇧🇯",
+	st_barthelemy: "🇧🇱",
+	bermuda: "🇧🇲",
+	brunei: "🇧🇳",
+	bolivia: "🇧🇴",
+	caribbean_netherlands: "🇧🇶",
+	brazil: "🇧🇷",
+	bahamas: "🇧🇸",
+	bhutan: "🇧🇹",
+	bouvet_island: "🇧🇻",
+	botswana: "🇧🇼",
+	belarus: "🇧🇾",
+	belize: "🇧🇿",
+	canada: "🇨🇦",
+	cocos_islands: "🇨🇨",
+	congo_kinshasa: "🇨🇩",
+	central_african_republic: "🇨🇫",
+	congo_brazzaville: "🇨🇬",
+	switzerland: "🇨🇭",
+	cote_divoire: "🇨🇮",
+	cook_islands: "🇨🇰",
+	chile: "🇨🇱",
+	cameroon: "🇨🇲",
+	cn: "🇨🇳",
+	colombia: "🇨🇴",
+	clipperton_island: "🇨🇵",
+	costa_rica: "🇨🇷",
+	cuba: "🇨🇺",
+	cape_verde: "🇨🇻",
+	curacao: "🇨🇼",
+	christmas_island: "🇨🇽",
+	cyprus: "🇨🇾",
+	czech_republic: "🇨🇿",
+	de: "🇩🇪",
+	diego_garcia: "🇩🇬",
+	djibouti: "🇩🇯",
+	denmark: "🇩🇰",
+	dominica: "🇩🇲",
+	dominican_republic: "🇩🇴",
+	algeria: "🇩🇿",
+	ceuta_melilla: "🇪🇦",
+	ecuador: "🇪🇨",
+	estonia: "🇪🇪",
+	egypt: "🇪🇬",
+	western_sahara: "🇪🇭",
+	eritrea: "🇪🇷",
+	es: "🇪🇸",
+	ethiopia: "🇪🇹",
+	eu: "🇪🇺",
+	european_union: "🇪🇺",
+	finland: "🇫🇮",
+	fiji: "🇫🇯",
+	falkland_islands: "🇫🇰",
+	micronesia: "🇫🇲",
+	faroe_islands: "🇫🇴",
+	fr: "🇫🇷",
+	gabon: "🇬🇦",
+	gb: "🇬🇧",
+	uk: "🇬🇧",
+	grenada: "🇬🇩",
+	georgia: "🇬🇪",
+	french_guiana: "🇬🇫",
+	guernsey: "🇬🇬",
+	ghana: "🇬🇭",
+	gibraltar: "🇬🇮",
+	greenland: "🇬🇱",
+	gambia: "🇬🇲",
+	guinea: "🇬🇳",
+	guadeloupe: "🇬🇵",
+	equatorial_guinea: "🇬🇶",
+	greece: "🇬🇷",
+	south_georgia_south_sandwich_islands: "🇬🇸",
+	guatemala: "🇬🇹",
+	guam: "🇬🇺",
+	guinea_bissau: "🇬🇼",
+	guyana: "🇬🇾",
+	hong_kong: "🇭🇰",
+	heard_mcdonald_islands: "🇭🇲",
+	honduras: "🇭🇳",
+	croatia: "🇭🇷",
+	haiti: "🇭🇹",
+	hungary: "🇭🇺",
+	canary_islands: "🇮🇨",
+	indonesia: "🇮🇩",
+	ireland: "🇮🇪",
+	israel: "🇮🇱",
+	isle_of_man: "🇮🇲",
+	india: "🇮🇳",
+	british_indian_ocean_territory: "🇮🇴",
+	iraq: "🇮🇶",
+	iran: "🇮🇷",
+	iceland: "🇮🇸",
+	it: "🇮🇹",
+	jersey: "🇯🇪",
+	jamaica: "🇯🇲",
+	jordan: "🇯🇴",
+	jp: "🇯🇵",
+	kenya: "🇰🇪",
+	kyrgyzstan: "🇰🇬",
+	cambodia: "🇰🇭",
+	kiribati: "🇰🇮",
+	comoros: "🇰🇲",
+	st_kitts_nevis: "🇰🇳",
+	north_korea: "🇰🇵",
+	kr: "🇰🇷",
+	kuwait: "🇰🇼",
+	cayman_islands: "🇰🇾",
+	kazakhstan: "🇰🇿",
+	laos: "🇱🇦",
+	lebanon: "🇱🇧",
+	st_lucia: "🇱🇨",
+	liechtenstein: "🇱🇮",
+	sri_lanka: "🇱🇰",
+	liberia: "🇱🇷",
+	lesotho: "🇱🇸",
+	lithuania: "🇱🇹",
+	luxembourg: "🇱🇺",
+	latvia: "🇱🇻",
+	libya: "🇱🇾",
+	morocco: "🇲🇦",
+	monaco: "🇲🇨",
+	moldova: "🇲🇩",
+	montenegro: "🇲🇪",
+	st_martin: "🇲🇫",
+	madagascar: "🇲🇬",
+	marshall_islands: "🇲🇭",
+	macedonia: "🇲🇰",
+	mali: "🇲🇱",
+	myanmar: "🇲🇲",
+	mongolia: "🇲🇳",
+	macau: "🇲🇴",
+	northern_mariana_islands: "🇲🇵",
+	martinique: "🇲🇶",
+	mauritania: "🇲🇷",
+	montserrat: "🇲🇸",
+	malta: "🇲🇹",
+	mauritius: "🇲🇺",
+	maldives: "🇲🇻",
+	malawi: "🇲🇼",
+	mexico: "🇲🇽",
+	malaysia: "🇲🇾",
+	mozambique: "🇲🇿",
+	namibia: "🇳🇦",
+	new_caledonia: "🇳🇨",
+	niger: "🇳🇪",
+	norfolk_island: "🇳🇫",
+	nigeria: "🇳🇬",
+	nicaragua: "🇳🇮",
+	netherlands: "🇳🇱",
+	norway: "🇳🇴",
+	nepal: "🇳🇵",
+	nauru: "🇳🇷",
+	niue: "🇳🇺",
+	new_zealand: "🇳🇿",
+	oman: "🇴🇲",
+	panama: "🇵🇦",
+	peru: "🇵🇪",
+	french_polynesia: "🇵🇫",
+	papua_new_guinea: "🇵🇬",
+	philippines: "🇵🇭",
+	pakistan: "🇵🇰",
+	poland: "🇵🇱",
+	st_pierre_miquelon: "🇵🇲",
+	pitcairn_islands: "🇵🇳",
+	puerto_rico: "🇵🇷",
+	palestinian_territories: "🇵🇸",
+	portugal: "🇵🇹",
+	palau: "🇵🇼",
+	paraguay: "🇵🇾",
+	qatar: "🇶🇦",
+	reunion: "🇷🇪",
+	romania: "🇷🇴",
+	serbia: "🇷🇸",
+	ru: "🇷🇺",
+	rwanda: "🇷🇼",
+	saudi_arabia: "🇸🇦",
+	solomon_islands: "🇸🇧",
+	seychelles: "🇸🇨",
+	sudan: "🇸🇩",
+	sweden: "🇸🇪",
+	singapore: "🇸🇬",
+	st_helena: "🇸🇭",
+	slovenia: "🇸🇮",
+	svalbard_jan_mayen: "🇸🇯",
+	slovakia: "🇸🇰",
+	sierra_leone: "🇸🇱",
+	san_marino: "🇸🇲",
+	senegal: "🇸🇳",
+	somalia: "🇸🇴",
+	suriname: "🇸🇷",
+	south_sudan: "🇸🇸",
+	sao_tome_principe: "🇸🇹",
+	el_salvador: "🇸🇻",
+	sint_maarten: "🇸🇽",
+	syria: "🇸🇾",
+	swaziland: "🇸🇿",
+	tristan_da_cunha: "🇹🇦",
+	turks_caicos_islands: "🇹🇨",
+	chad: "🇹🇩",
+	french_southern_territories: "🇹🇫",
+	togo: "🇹🇬",
+	thailand: "🇹🇭",
+	tajikistan: "🇹🇯",
+	tokelau: "🇹🇰",
+	timor_leste: "🇹🇱",
+	turkmenistan: "🇹🇲",
+	tunisia: "🇹🇳",
+	tonga: "🇹🇴",
+	tr: "🇹🇷",
+	trinidad_tobago: "🇹🇹",
+	tuvalu: "🇹🇻",
+	taiwan: "🇹🇼",
+	tanzania: "🇹🇿",
+	ukraine: "🇺🇦",
+	uganda: "🇺🇬",
+	us_outlying_islands: "🇺🇲",
+	united_nations: "🇺🇳",
+	us: "🇺🇸",
+	uruguay: "🇺🇾",
+	uzbekistan: "🇺🇿",
+	vatican_city: "🇻🇦",
+	st_vincent_grenadines: "🇻🇨",
+	venezuela: "🇻🇪",
+	british_virgin_islands: "🇻🇬",
+	us_virgin_islands: "🇻🇮",
+	vietnam: "🇻🇳",
+	vanuatu: "🇻🇺",
+	wallis_futuna: "🇼🇫",
+	samoa: "🇼🇸",
+	kosovo: "🇽🇰",
+	yemen: "🇾🇪",
+	mayotte: "🇾🇹",
+	south_africa: "🇿🇦",
+	zambia: "🇿🇲",
+	zimbabwe: "🇿🇼",
+	england: "🏴󠁧󠁢󠁥󠁮󠁧󠁿",
+	scotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿",
+	wales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿",
+};
diff --git a/src/assets/sounds/Audio.ts b/src/assets/sounds/Audio.ts
index 1168c6c614fe1d436c2f5e4429666f5a29ce74e3..be4881b3439ac821677c0ed8f0db2e426c2eb2a2 100644
--- a/src/assets/sounds/Audio.ts
+++ b/src/assets/sounds/Audio.ts
@@ -1,24 +1,29 @@
-import message from './message.mp3';
-import outbound from './outbound.mp3';
-import call_join from './call_join.mp3';
-import call_leave from './call_leave.mp3';
+import call_join from "./call_join.mp3";
+import call_leave from "./call_leave.mp3";
+import message from "./message.mp3";
+import outbound from "./outbound.mp3";
 
 const SoundMap: { [key in Sounds]: string } = {
-    message,
-    outbound,
-    call_join,
-    call_leave
-}
+	message,
+	outbound,
+	call_join,
+	call_leave,
+};
 
-export type Sounds = 'message' | 'outbound' | 'call_join' | 'call_leave';
-export const SOUNDS_ARRAY: Sounds[] = [ 'message', 'outbound', 'call_join', 'call_leave' ];
+export type Sounds = "message" | "outbound" | "call_join" | "call_leave";
+export const SOUNDS_ARRAY: Sounds[] = [
+	"message",
+	"outbound",
+	"call_join",
+	"call_leave",
+];
 
 export function playSound(sound: Sounds) {
-    let file = SoundMap[sound];
-    let el = new Audio(file);
-    try {
-        el.play();
-    } catch (err) {
-        console.error('Failed to play audio file', file, err);
-    }
+	let file = SoundMap[sound];
+	let el = new Audio(file);
+	try {
+		el.play();
+	} catch (err) {
+		console.error("Failed to play audio file", file, err);
+	}
 }
diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx
index 565f4a4ac2fd34f8ba0694aaaf61c088f457c47e..419a969c66e17f42b360df4add3fb85131ff3c1d 100644
--- a/src/components/common/AutoComplete.tsx
+++ b/src/components/common/AutoComplete.tsx
@@ -1,449 +1,475 @@
+import { SYSTEM_USER_ID, User } from "revolt.js";
+import { Channels } from "revolt.js/dist/api/objects";
+import styled, { css } from "styled-components";
+
 import { StateUpdater, useContext, useState } from "preact/hooks";
+
 import { AppContext } from "../../context/revoltjs/RevoltClient";
-import { Channels } from "revolt.js/dist/api/objects";
+
 import { emojiDictionary } from "../../assets/emojis";
-import { SYSTEM_USER_ID, User } from "revolt.js";
-import UserIcon from "./user/UserIcon";
-import styled, { css } from "styled-components";
-import Emoji from "./Emoji";
 import ChannelIcon from "./ChannelIcon";
+import Emoji from "./Emoji";
+import UserIcon from "./user/UserIcon";
 
 export type AutoCompleteState =
-    | { type: "none" }
-    | ({ selected: number; within: boolean; } & (
-        {
-            type: "emoji";
-            matches: string[];
-        } |
-        {
-            type: "user";
-            matches: User[];
-        } |
-        {
-            type: "channel";
-            matches: Channels.TextChannel[];
-        }
-    ));
+	| { type: "none" }
+	| ({ selected: number; within: boolean } & (
+			| {
+					type: "emoji";
+					matches: string[];
+			  }
+			| {
+					type: "user";
+					matches: User[];
+			  }
+			| {
+					type: "channel";
+					matches: Channels.TextChannel[];
+			  }
+	  ));
 
 export type SearchClues = {
-    users?: { type: 'channel', id: string } | { type: 'all' },
-    channels?: { server: string }
+	users?: { type: "channel"; id: string } | { type: "all" };
+	channels?: { server: string };
 };
 
 export type AutoCompleteProps = {
-    detached?: boolean,
-    state: AutoCompleteState,
-    setState: StateUpdater<AutoCompleteState>,
-
-    onKeyUp: (ev: KeyboardEvent) => void,
-    onKeyDown: (ev: KeyboardEvent) => boolean,
-    onChange: (ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => void,
-    onClick: JSX.MouseEventHandler<HTMLButtonElement>,
-    onFocus: JSX.FocusEventHandler<HTMLTextAreaElement>,
-    onBlur: JSX.FocusEventHandler<HTMLTextAreaElement>
-}
+	detached?: boolean;
+	state: AutoCompleteState;
+	setState: StateUpdater<AutoCompleteState>;
+
+	onKeyUp: (ev: KeyboardEvent) => void;
+	onKeyDown: (ev: KeyboardEvent) => boolean;
+	onChange: (ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => void;
+	onClick: JSX.MouseEventHandler<HTMLButtonElement>;
+	onFocus: JSX.FocusEventHandler<HTMLTextAreaElement>;
+	onBlur: JSX.FocusEventHandler<HTMLTextAreaElement>;
+};
 
-export function useAutoComplete(setValue: (v?: string) => void, searchClues?: SearchClues): AutoCompleteProps {
-    const [state, setState] = useState<AutoCompleteState>({ type: 'none' });
-    const [focused, setFocused] = useState(false);
-    const client = useContext(AppContext);
-
-    function findSearchString(
-        el: HTMLTextAreaElement
-    ): ["emoji" | "user" | "channel", string, number] | undefined {
-        if (el.selectionStart === el.selectionEnd) {
-            let cursor = el.selectionStart;
-            let content = el.value.slice(0, cursor);
-
-            let valid = /\w/;
-
-            let j = content.length - 1;
-            if (content[j] === '@') {
-                return [
-                    "user",
-                    "",
-                    j
-                ];
-            } else if (content[j] === '#') {
-                return [
-                    "channel",
-                    "",
-                    j
-                ];
-            }
-
-            while (j >= 0 && valid.test(content[j])) {
-                j--;
-            }
-
-            if (j === -1) return;
-            let current = content[j];
-
-            if (current === ":" || current === "@" || current === "#") {
-                let search = content.slice(j + 1, content.length);
-                if (search.length > 0) {
-                    return [
-                        current === "#" ? "channel" :
-                        current === ":" ? "emoji" : "user",
-                        search.toLowerCase(),
-                        j + 1
-                    ];
-                }
-            }
-        }
-    }
-    
-    function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
-        const el = ev.currentTarget;
-
-        let result = findSearchString(el);
-        if (result) {
-            let [type, search] = result;
-            const regex = new RegExp(search, 'i');
-
-            if (type === "emoji") {
-                // ! FIXME: we should convert it to a Binary Search Tree and use that
-                let matches = Object.keys(emojiDictionary)
-                    .filter((emoji: string) => emoji.match(regex))
-                    .splice(0, 5);
-
-                if (matches.length > 0) {
-                    let currentPosition =
-                        state.type !== "none"
-                            ? state.selected
-                            : 0;
-                    
-                    setState({
-                        type: "emoji",
-                        matches,
-                        selected: Math.min(currentPosition, matches.length - 1),
-                        within: false
-                    });
-
-                    return;
-                }
-            }
-
-            if (type === "user" && searchClues?.users) {
-                let users: User[] = [];
-                switch (searchClues.users.type) {
-                    case 'all': users = client.users.toArray(); break;
-                    case 'channel': {
-                        let channel = client.channels.get(searchClues.users.id);
-                        switch (channel?.channel_type) {
-                            case 'Group':
-                            case 'DirectMessage':
-                                users = client.users.mapKeys(channel.recipients)
-                                    .filter(x => typeof x !== 'undefined') as User[];
-                                break;
-                            case 'TextChannel':
-                                const server = channel.server;
-                                users = client.servers.members.toArray()
-                                    .filter(x => x._id.substr(0, 26) === server)
-                                    .map(x => client.users.get(x._id.substr(26)))
-                                    .filter(x => typeof x !== 'undefined') as User[];
-                                break;
-                            default: return;
-                        }
-                    }
-                }
-
-                users = users.filter(x => x._id !== SYSTEM_USER_ID);
-
-                let matches = (search.length > 0 ? users.filter(user => user.username.toLowerCase().match(regex)) : users)
-                    .splice(0, 5)
-                    .filter(x => typeof x !== "undefined");
-
-                if (matches.length > 0) {
-                    let currentPosition =
-                        state.type !== "none"
-                            ? state.selected
-                            : 0;
-                    
-                    setState({
-                        type: "user",
-                        matches,
-                        selected: Math.min(currentPosition, matches.length - 1),
-                        within: false
-                    });
-
-                    return;
-                }
-            }
-
-            if (type === 'channel' && searchClues?.channels) {
-                let channels = client.servers.get(searchClues.channels.server)
-                    ?.channels
-                    .map(x => client.channels.get(x))
-                    .filter(x => typeof x !== 'undefined') as Channels.TextChannel[];
-
-                let matches = (search.length > 0 ? channels.filter(channel => channel.name.toLowerCase().match(regex)) : channels)
-                    .splice(0, 5)
-                    .filter(x => typeof x !== "undefined");
-
-                if (matches.length > 0) {
-                    let currentPosition =
-                        state.type !== "none"
-                            ? state.selected
-                            : 0;
-                    
-                    setState({
-                        type: "channel",
-                        matches,
-                        selected: Math.min(currentPosition, matches.length - 1),
-                        within: false
-                    });
-
-                    return;
-                }
-            }
-        }
-
-        if (state.type !== "none") {
-            setState({ type: "none" });
-        }
-    }
-
-    function selectCurrent(el: HTMLTextAreaElement) {
-        if (state.type !== "none") {
-            let result = findSearchString(el);
-            if (result) {
-                let [_type, search, index] = result;
-
-                let content = el.value.split("");
-                if (state.type === "emoji") {
-                    content.splice(
-                        index,
-                        search.length,
-                        state.matches[state.selected],
-                        ": "
-                    );
-                } else if (state.type === 'user') {
-                    content.splice(
-                        index - 1,
-                        search.length + 1,
-                        "<@",
-                        state.matches[state.selected]._id,
-                        "> "
-                    );
-                } else {
-                    content.splice(
-                        index - 1,
-                        search.length + 1,
-                        "<#",
-                        state.matches[state.selected]._id,
-                        "> "
-                    );
-                }
-
-                setValue(content.join(""));
-            }
-        }
-    }
-
-    function onClick(ev: JSX.TargetedMouseEvent<HTMLButtonElement>) {
-        ev.preventDefault();
-        selectCurrent(document.querySelector("#message")!);
-    }
-
-    function onKeyDown(e: KeyboardEvent) {
-        if (focused && state.type !== 'none') {
-            if (e.key === "ArrowUp") {
-                e.preventDefault();
-                if (state.selected > 0) {
-                    setState({
-                        ...state,
-                        selected: state.selected - 1
-                    });
-                }
-
-                return true;
-            }
-
-            if (e.key === "ArrowDown") {
-                e.preventDefault();
-                if (state.selected < state.matches.length - 1) {
-                    setState({
-                        ...state,
-                        selected: state.selected + 1
-                    });
-                }
-
-                return true;
-            }
-
-            if (e.key === "Enter" || e.key === "Tab") {
-                e.preventDefault();
-                selectCurrent(
-                    e.currentTarget as HTMLTextAreaElement
-                );
-
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    function onKeyUp(e: KeyboardEvent) {
-        if (e.currentTarget !== null) {
-            // @ts-expect-error
-            onChange(e);
-        }
-    }
-
-    function onFocus(ev: JSX.TargetedFocusEvent<HTMLTextAreaElement>) {
-        setFocused(true);
-        onChange(ev);
-    }
-
-    function onBlur() {
-        if (state.type !== 'none' && state.within) return;
-        setFocused(false);
-    }
-
-    return {
-        state: focused ? state : { type: 'none' },
-        setState,
-
-        onClick,
-        onChange,
-        onKeyUp,
-        onKeyDown,
-        onFocus,
-        onBlur
-    }
+export function useAutoComplete(
+	setValue: (v?: string) => void,
+	searchClues?: SearchClues,
+): AutoCompleteProps {
+	const [state, setState] = useState<AutoCompleteState>({ type: "none" });
+	const [focused, setFocused] = useState(false);
+	const client = useContext(AppContext);
+
+	function findSearchString(
+		el: HTMLTextAreaElement,
+	): ["emoji" | "user" | "channel", string, number] | undefined {
+		if (el.selectionStart === el.selectionEnd) {
+			let cursor = el.selectionStart;
+			let content = el.value.slice(0, cursor);
+
+			let valid = /\w/;
+
+			let j = content.length - 1;
+			if (content[j] === "@") {
+				return ["user", "", j];
+			} else if (content[j] === "#") {
+				return ["channel", "", j];
+			}
+
+			while (j >= 0 && valid.test(content[j])) {
+				j--;
+			}
+
+			if (j === -1) return;
+			let current = content[j];
+
+			if (current === ":" || current === "@" || current === "#") {
+				let search = content.slice(j + 1, content.length);
+				if (search.length > 0) {
+					return [
+						current === "#"
+							? "channel"
+							: current === ":"
+							? "emoji"
+							: "user",
+						search.toLowerCase(),
+						j + 1,
+					];
+				}
+			}
+		}
+	}
+
+	function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
+		const el = ev.currentTarget;
+
+		let result = findSearchString(el);
+		if (result) {
+			let [type, search] = result;
+			const regex = new RegExp(search, "i");
+
+			if (type === "emoji") {
+				// ! FIXME: we should convert it to a Binary Search Tree and use that
+				let matches = Object.keys(emojiDictionary)
+					.filter((emoji: string) => emoji.match(regex))
+					.splice(0, 5);
+
+				if (matches.length > 0) {
+					let currentPosition =
+						state.type !== "none" ? state.selected : 0;
+
+					setState({
+						type: "emoji",
+						matches,
+						selected: Math.min(currentPosition, matches.length - 1),
+						within: false,
+					});
+
+					return;
+				}
+			}
+
+			if (type === "user" && searchClues?.users) {
+				let users: User[] = [];
+				switch (searchClues.users.type) {
+					case "all":
+						users = client.users.toArray();
+						break;
+					case "channel": {
+						let channel = client.channels.get(searchClues.users.id);
+						switch (channel?.channel_type) {
+							case "Group":
+							case "DirectMessage":
+								users = client.users
+									.mapKeys(channel.recipients)
+									.filter(
+										(x) => typeof x !== "undefined",
+									) as User[];
+								break;
+							case "TextChannel":
+								const server = channel.server;
+								users = client.servers.members
+									.toArray()
+									.filter(
+										(x) => x._id.substr(0, 26) === server,
+									)
+									.map((x) =>
+										client.users.get(x._id.substr(26)),
+									)
+									.filter(
+										(x) => typeof x !== "undefined",
+									) as User[];
+								break;
+							default:
+								return;
+						}
+					}
+				}
+
+				users = users.filter((x) => x._id !== SYSTEM_USER_ID);
+
+				let matches = (
+					search.length > 0
+						? users.filter((user) =>
+								user.username.toLowerCase().match(regex),
+						  )
+						: users
+				)
+					.splice(0, 5)
+					.filter((x) => typeof x !== "undefined");
+
+				if (matches.length > 0) {
+					let currentPosition =
+						state.type !== "none" ? state.selected : 0;
+
+					setState({
+						type: "user",
+						matches,
+						selected: Math.min(currentPosition, matches.length - 1),
+						within: false,
+					});
+
+					return;
+				}
+			}
+
+			if (type === "channel" && searchClues?.channels) {
+				let channels = client.servers
+					.get(searchClues.channels.server)
+					?.channels.map((x) => client.channels.get(x))
+					.filter(
+						(x) => typeof x !== "undefined",
+					) as Channels.TextChannel[];
+
+				let matches = (
+					search.length > 0
+						? channels.filter((channel) =>
+								channel.name.toLowerCase().match(regex),
+						  )
+						: channels
+				)
+					.splice(0, 5)
+					.filter((x) => typeof x !== "undefined");
+
+				if (matches.length > 0) {
+					let currentPosition =
+						state.type !== "none" ? state.selected : 0;
+
+					setState({
+						type: "channel",
+						matches,
+						selected: Math.min(currentPosition, matches.length - 1),
+						within: false,
+					});
+
+					return;
+				}
+			}
+		}
+
+		if (state.type !== "none") {
+			setState({ type: "none" });
+		}
+	}
+
+	function selectCurrent(el: HTMLTextAreaElement) {
+		if (state.type !== "none") {
+			let result = findSearchString(el);
+			if (result) {
+				let [_type, search, index] = result;
+
+				let content = el.value.split("");
+				if (state.type === "emoji") {
+					content.splice(
+						index,
+						search.length,
+						state.matches[state.selected],
+						": ",
+					);
+				} else if (state.type === "user") {
+					content.splice(
+						index - 1,
+						search.length + 1,
+						"<@",
+						state.matches[state.selected]._id,
+						"> ",
+					);
+				} else {
+					content.splice(
+						index - 1,
+						search.length + 1,
+						"<#",
+						state.matches[state.selected]._id,
+						"> ",
+					);
+				}
+
+				setValue(content.join(""));
+			}
+		}
+	}
+
+	function onClick(ev: JSX.TargetedMouseEvent<HTMLButtonElement>) {
+		ev.preventDefault();
+		selectCurrent(document.querySelector("#message")!);
+	}
+
+	function onKeyDown(e: KeyboardEvent) {
+		if (focused && state.type !== "none") {
+			if (e.key === "ArrowUp") {
+				e.preventDefault();
+				if (state.selected > 0) {
+					setState({
+						...state,
+						selected: state.selected - 1,
+					});
+				}
+
+				return true;
+			}
+
+			if (e.key === "ArrowDown") {
+				e.preventDefault();
+				if (state.selected < state.matches.length - 1) {
+					setState({
+						...state,
+						selected: state.selected + 1,
+					});
+				}
+
+				return true;
+			}
+
+			if (e.key === "Enter" || e.key === "Tab") {
+				e.preventDefault();
+				selectCurrent(e.currentTarget as HTMLTextAreaElement);
+
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	function onKeyUp(e: KeyboardEvent) {
+		if (e.currentTarget !== null) {
+			// @ts-expect-error
+			onChange(e);
+		}
+	}
+
+	function onFocus(ev: JSX.TargetedFocusEvent<HTMLTextAreaElement>) {
+		setFocused(true);
+		onChange(ev);
+	}
+
+	function onBlur() {
+		if (state.type !== "none" && state.within) return;
+		setFocused(false);
+	}
+
+	return {
+		state: focused ? state : { type: "none" },
+		setState,
+
+		onClick,
+		onChange,
+		onKeyUp,
+		onKeyDown,
+		onFocus,
+		onBlur,
+	};
 }
 
 const Base = styled.div<{ detached?: boolean }>`
-    position: relative;
-
-    > div {
-        bottom: 0;
-        width: 100%;
-        position: absolute;
-        background: var(--primary-header);
-    }
-
-    button {
-        gap: 8px;
-        margin: 4px;
-        padding: 6px;
-        border: none;
-        display: flex;
-        font-size: 1em;
-        cursor: pointer;
-        border-radius: 6px;
-        align-items: center;
-        flex-direction: row;
-        background: transparent;
-        color: var(--foreground);
-        width: calc(100% - 12px);
-
-        span {
-            display: grid;
-            place-items: center;
-        }
-
-        &.active {
-            background: var(--primary-background);
-        }
-    }
-
-    ${ props => props.detached && css`
-        bottom: 8px;
-
-        > div {
-            border-radius: 4px;
-        }
-    ` }
+	position: relative;
+
+	> div {
+		bottom: 0;
+		width: 100%;
+		position: absolute;
+		background: var(--primary-header);
+	}
+
+	button {
+		gap: 8px;
+		margin: 4px;
+		padding: 6px;
+		border: none;
+		display: flex;
+		font-size: 1em;
+		cursor: pointer;
+		border-radius: 6px;
+		align-items: center;
+		flex-direction: row;
+		background: transparent;
+		color: var(--foreground);
+		width: calc(100% - 12px);
+
+		span {
+			display: grid;
+			place-items: center;
+		}
+
+		&.active {
+			background: var(--primary-background);
+		}
+	}
+
+	${(props) =>
+		props.detached &&
+		css`
+			bottom: 8px;
+
+			> div {
+				border-radius: 4px;
+			}
+		`}
 `;
 
-export default function AutoComplete({ detached, state, setState, onClick }: Pick<AutoCompleteProps, 'detached' | 'state' | 'setState' | 'onClick'>) {
-    return (
-        <Base detached={detached}>
-            <div>
-                {state.type === "emoji" &&
-                    state.matches.map((match, i) => (
-                        <button
-                            className={i === state.selected ? "active" : ''}
-                            onMouseEnter={() =>
-                                (i !== state.selected ||
-                                    !state.within) &&
-                                    setState({
-                                    ...state,
-                                    selected: i,
-                                    within: true
-                                })
-                            }
-                            onMouseLeave={() =>
-                                state.within &&
-                                setState({
-                                    ...state,
-                                    within: false
-                                })
-                            }
-                            onClick={onClick}>
-                            <Emoji emoji={(emojiDictionary as Record<string, string>)[match]} size={20} />
-                            :{match}:
-                        </button>
-                    ))}
-                {state.type === "user" &&
-                    state.matches.map((match, i) => (
-                        <button
-                            className={i === state.selected ? "active" : ''}
-                            onMouseEnter={() =>
-                                (i !== state.selected ||
-                                    !state.within) &&
-                                setState({
-                                    ...state,
-                                    selected: i,
-                                    within: true
-                                })
-                            }
-                            onMouseLeave={() =>
-                                state.within &&
-                                setState({
-                                    ...state,
-                                    within: false
-                                })
-                            }
-                            onClick={onClick}>
-                            <UserIcon
-                                size={24}
-                                target={match}
-                                status={true} />
-                            {match.username}
-                        </button>
-                    ))}
-                {state.type === "channel" &&
-                    state.matches.map((match, i) => (
-                        <button
-                            className={i === state.selected ? "active" : ''}
-                            onMouseEnter={() =>
-                                (i !== state.selected ||
-                                    !state.within) &&
-                                setState({
-                                    ...state,
-                                    selected: i,
-                                    within: true
-                                })
-                            }
-                            onMouseLeave={() =>
-                                state.within &&
-                                setState({
-                                    ...state,
-                                    within: false
-                                })
-                            }
-                            onClick={onClick}>
-                            <ChannelIcon
-                                size={24}
-                                target={match} />
-                            {match.name}
-                        </button>
-                    ))}
-            </div>
-        </Base>
-    )
+export default function AutoComplete({
+	detached,
+	state,
+	setState,
+	onClick,
+}: Pick<AutoCompleteProps, "detached" | "state" | "setState" | "onClick">) {
+	return (
+		<Base detached={detached}>
+			<div>
+				{state.type === "emoji" &&
+					state.matches.map((match, i) => (
+						<button
+							className={i === state.selected ? "active" : ""}
+							onMouseEnter={() =>
+								(i !== state.selected || !state.within) &&
+								setState({
+									...state,
+									selected: i,
+									within: true,
+								})
+							}
+							onMouseLeave={() =>
+								state.within &&
+								setState({
+									...state,
+									within: false,
+								})
+							}
+							onClick={onClick}>
+							<Emoji
+								emoji={
+									(emojiDictionary as Record<string, string>)[
+										match
+									]
+								}
+								size={20}
+							/>
+							:{match}:
+						</button>
+					))}
+				{state.type === "user" &&
+					state.matches.map((match, i) => (
+						<button
+							className={i === state.selected ? "active" : ""}
+							onMouseEnter={() =>
+								(i !== state.selected || !state.within) &&
+								setState({
+									...state,
+									selected: i,
+									within: true,
+								})
+							}
+							onMouseLeave={() =>
+								state.within &&
+								setState({
+									...state,
+									within: false,
+								})
+							}
+							onClick={onClick}>
+							<UserIcon size={24} target={match} status={true} />
+							{match.username}
+						</button>
+					))}
+				{state.type === "channel" &&
+					state.matches.map((match, i) => (
+						<button
+							className={i === state.selected ? "active" : ""}
+							onMouseEnter={() =>
+								(i !== state.selected || !state.within) &&
+								setState({
+									...state,
+									selected: i,
+									within: true,
+								})
+							}
+							onMouseLeave={() =>
+								state.within &&
+								setState({
+									...state,
+									within: false,
+								})
+							}
+							onClick={onClick}>
+							<ChannelIcon size={24} target={match} />
+							{match.name}
+						</button>
+					))}
+			</div>
+		</Base>
+	);
 }
diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx
index c0d596ed52a9ed749db85725cb966cd8994cd1a6..e3dd136bba719bf9ddb49844dad7f2746be99f5a 100644
--- a/src/components/common/ChannelIcon.tsx
+++ b/src/components/common/ChannelIcon.tsx
@@ -1,43 +1,65 @@
-import { useContext } from "preact/hooks";
-import { Channels } from "revolt.js/dist/api/objects";
 import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
-import { ImageIconBase, IconBaseProps } from "./IconBase";
+import { Channels } from "revolt.js/dist/api/objects";
+
+import { useContext } from "preact/hooks";
+
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 
-interface Props extends IconBaseProps<Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel> {
-    isServerChannel?: boolean;
+import { ImageIconBase, IconBaseProps } from "./IconBase";
+import fallback from "./assets/group.png";
+
+interface Props
+	extends IconBaseProps<
+		Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel
+	> {
+	isServerChannel?: boolean;
 }
 
-import fallback from './assets/group.png';
-
-export default function ChannelIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) {
-    const client = useContext(AppContext);
-
-    const { size, target, attachment, isServerChannel: server, animate, children, as, ...imgProps } = props;
-    const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
-    const isServerChannel = server || (target && (target.channel_type === 'TextChannel' || target.channel_type === 'VoiceChannel'));
-
-    if (typeof iconURL === 'undefined') {
-        if (isServerChannel) {
-            if (target?.channel_type === 'VoiceChannel') {
-                return (
-                    <VolumeFull size={size} />
-                )
-            } else {
-                return (
-                    <Hash size={size} />
-                )
-            }
-        }
-    }
-
-    return (
-        // ! fixme: replace fallback with <picture /> + <source />
-        <ImageIconBase {...imgProps}
-            width={size}
-            height={size}
-            aria-hidden="true"
-            square={isServerChannel}
-            src={iconURL ?? fallback} />
-    );
+export default function ChannelIcon(
+	props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
+) {
+	const client = useContext(AppContext);
+
+	const {
+		size,
+		target,
+		attachment,
+		isServerChannel: server,
+		animate,
+		children,
+		as,
+		...imgProps
+	} = props;
+	const iconURL = client.generateFileURL(
+		target?.icon ?? attachment,
+		{ max_side: 256 },
+		animate,
+	);
+	const isServerChannel =
+		server ||
+		(target &&
+			(target.channel_type === "TextChannel" ||
+				target.channel_type === "VoiceChannel"));
+
+	if (typeof iconURL === "undefined") {
+		if (isServerChannel) {
+			if (target?.channel_type === "VoiceChannel") {
+				return <VolumeFull size={size} />;
+			} else {
+				return <Hash size={size} />;
+			}
+		}
+	}
+
+	return (
+		// ! fixme: replace fallback with <picture /> + <source />
+		<ImageIconBase
+			{...imgProps}
+			width={size}
+			height={size}
+			aria-hidden="true"
+			square={isServerChannel}
+			src={iconURL ?? fallback}
+		/>
+	);
 }
diff --git a/src/components/common/CollapsibleSection.tsx b/src/components/common/CollapsibleSection.tsx
index 3b6409fb9a32b916f697110693ea57b5890e9302..3bd20db000806983ce48229bf07ab43e1fd23488 100644
--- a/src/components/common/CollapsibleSection.tsx
+++ b/src/components/common/CollapsibleSection.tsx
@@ -1,50 +1,59 @@
-import Details from "../ui/Details";
+import { ChevronDown } from "@styled-icons/boxicons-regular";
+
 import { State, store } from "../../redux";
 import { Action } from "../../redux/reducers";
+
+import Details from "../ui/Details";
+
 import { Children } from "../../types/Preact";
-import { ChevronDown } from "@styled-icons/boxicons-regular";
 
 interface Props {
-    id: string;
-    defaultValue: boolean;
+	id: string;
+	defaultValue: boolean;
 
-    sticky?: boolean;
-    large?: boolean;
+	sticky?: boolean;
+	large?: boolean;
 
-    summary: Children;
-    children: Children;
+	summary: Children;
+	children: Children;
 }
 
-export default function CollapsibleSection({ id, defaultValue, summary, children, ...detailsProps }: Props) {
-    const state: State = store.getState();
-
-    function setState(state: boolean) {
-        if (state === defaultValue) {
-            store.dispatch({
-                type: 'SECTION_TOGGLE_UNSET',
-                id
-            } as Action);
-        } else {
-            store.dispatch({
-                type: 'SECTION_TOGGLE_SET',
-                id,
-                state
-            } as Action);
-        }
-    }
-
-    return (
-        <Details
-            open={state.sectionToggle[id] ?? defaultValue}
-            onToggle={e => setState(e.currentTarget.open)}
-            {...detailsProps}>
-            <summary>
-                <div class="padding">
-                    <ChevronDown size={20} />
-                    { summary }
-                </div>
-            </summary>
-            { children }
-        </Details>
-    )
+export default function CollapsibleSection({
+	id,
+	defaultValue,
+	summary,
+	children,
+	...detailsProps
+}: Props) {
+	const state: State = store.getState();
+
+	function setState(state: boolean) {
+		if (state === defaultValue) {
+			store.dispatch({
+				type: "SECTION_TOGGLE_UNSET",
+				id,
+			} as Action);
+		} else {
+			store.dispatch({
+				type: "SECTION_TOGGLE_SET",
+				id,
+				state,
+			} as Action);
+		}
+	}
+
+	return (
+		<Details
+			open={state.sectionToggle[id] ?? defaultValue}
+			onToggle={(e) => setState(e.currentTarget.open)}
+			{...detailsProps}>
+			<summary>
+				<div class="padding">
+					<ChevronDown size={20} />
+					{summary}
+				</div>
+			</summary>
+			{children}
+		</Details>
+	);
 }
diff --git a/src/components/common/Emoji.tsx b/src/components/common/Emoji.tsx
index 791a0036dcbb0f0a2bc002e11f9d57e61582d0cd..f27be2f5d223333de662c755ab020dc3360e28c2 100644
--- a/src/components/common/Emoji.tsx
+++ b/src/components/common/Emoji.tsx
@@ -1,62 +1,72 @@
-import { EmojiPacks } from '../../redux/reducers/settings';
+import { EmojiPacks } from "../../redux/reducers/settings";
 
-var EMOJI_PACK = 'mutant';
+var EMOJI_PACK = "mutant";
 const REVISION = 3;
 
 export function setEmojiPack(pack: EmojiPacks) {
-    EMOJI_PACK = pack;
+	EMOJI_PACK = pack;
 }
 
 // Originally taken from Twemoji source code,
 // re-written by bree to be more readable.
 function codePoints(rune: string) {
-    const pairs = [];
-    let low = 0;
-    let i = 0;
-    
-    while (i < rune.length) {
-        const charCode = rune.charCodeAt(i++);
-        if (low) {
-            pairs.push(0x10000 + ((low - 0xd800) << 10) + (charCode - 0xdc00));
-            low = 0;
-        } else if (0xd800 <= charCode && charCode <= 0xdbff) {
-            low = charCode;
-        } else {
-            pairs.push(charCode);
-        }
-    }
-    
-    return pairs;
+	const pairs = [];
+	let low = 0;
+	let i = 0;
+
+	while (i < rune.length) {
+		const charCode = rune.charCodeAt(i++);
+		if (low) {
+			pairs.push(0x10000 + ((low - 0xd800) << 10) + (charCode - 0xdc00));
+			low = 0;
+		} else if (0xd800 <= charCode && charCode <= 0xdbff) {
+			low = charCode;
+		} else {
+			pairs.push(charCode);
+		}
+	}
+
+	return pairs;
 }
 
 // Taken from Twemoji source code.
 // scripts/build.js#344
 // grabTheRightIcon(rawText);
 const UFE0Fg = /\uFE0F/g;
-const U200D = String.fromCharCode(0x200D);
+const U200D = String.fromCharCode(0x200d);
 function toCodePoint(rune: string) {
-    return codePoints(rune.indexOf(U200D) < 0 ? rune.replace(UFE0Fg, '') : rune)
-        .map((val) => val.toString(16))
-        .join("-")
+	return codePoints(rune.indexOf(U200D) < 0 ? rune.replace(UFE0Fg, "") : rune)
+		.map((val) => val.toString(16))
+		.join("-");
 }
 
 function parseEmoji(emoji: string) {
-    let codepoint = toCodePoint(emoji);
-    return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
+	let codepoint = toCodePoint(emoji);
+	return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
 }
 
-export default function Emoji({ emoji, size }: { emoji: string, size?: number }) {
-    return (
-        <img
-            alt={emoji}
-            className="emoji"
-            draggable={false}
-            src={parseEmoji(emoji)}
-            style={size ? { width: `${size}px`, height: `${size}px` } : undefined}
-        />
-    )
+export default function Emoji({
+	emoji,
+	size,
+}: {
+	emoji: string;
+	size?: number;
+}) {
+	return (
+		<img
+			alt={emoji}
+			className="emoji"
+			draggable={false}
+			src={parseEmoji(emoji)}
+			style={
+				size ? { width: `${size}px`, height: `${size}px` } : undefined
+			}
+		/>
+	);
 }
 
 export function generateEmoji(emoji: string) {
-    return `<img class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(emoji)}" />`;
+	return `<img class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(
+		emoji,
+	)}" />`;
 }
diff --git a/src/components/common/IconBase.tsx b/src/components/common/IconBase.tsx
index acb5387ea9216d2af1346c8a955ab90fe2641662..56640d90edcaa277fdef7c776a37f005a60f8254 100644
--- a/src/components/common/IconBase.tsx
+++ b/src/components/common/IconBase.tsx
@@ -2,36 +2,40 @@ import { Attachment } from "revolt.js/dist/api/objects";
 import styled, { css } from "styled-components";
 
 export interface IconBaseProps<T> {
-    target?: T;
-    attachment?: Attachment;
+	target?: T;
+	attachment?: Attachment;
 
-    size: number;
-    animate?: boolean;
+	size: number;
+	animate?: boolean;
 }
 
 interface IconModifiers {
-    square?: boolean
+	square?: boolean;
 }
 
 export default styled.svg<IconModifiers>`
-    flex-shrink: 0;
-
-    img {
-        width: 100%;
-        height: 100%;
-        object-fit: cover;
-
-        ${ props => !props.square && css`
-            border-radius: 50%;
-        ` }
-    }
+	flex-shrink: 0;
+
+	img {
+		width: 100%;
+		height: 100%;
+		object-fit: cover;
+
+		${(props) =>
+			!props.square &&
+			css`
+				border-radius: 50%;
+			`}
+	}
 `;
 
 export const ImageIconBase = styled.img<IconModifiers>`
-    flex-shrink: 0;
-    object-fit: cover;
-
-    ${ props => !props.square && css`
-        border-radius: 50%;
-    ` }
+	flex-shrink: 0;
+	object-fit: cover;
+
+	${(props) =>
+		!props.square &&
+		css`
+			border-radius: 50%;
+		`}
 `;
diff --git a/src/components/common/LocaleSelector.tsx b/src/components/common/LocaleSelector.tsx
index 7f91b1c7a3063eec5f63dd373c797dee3819af81..c214e6bf9a54b0914eae48c7c737aec0dae3ca2b 100644
--- a/src/components/common/LocaleSelector.tsx
+++ b/src/components/common/LocaleSelector.tsx
@@ -1,40 +1,38 @@
-import ComboBox from "../ui/ComboBox";
 import { dispatch } from "../../redux";
 import { connectState } from "../../redux/connector";
-import { Language, LanguageEntry, Languages } from "../../context/Locale";
+
+import { Language, Languages } from "../../context/Locale";
+
+import ComboBox from "../ui/ComboBox";
 
 type Props = {
-    locale: string;
+	locale: string;
 };
 
 export function LocaleSelector(props: Props) {
-    return (
-        <ComboBox
-            value={props.locale}
-            onChange={e =>
-                dispatch({
-                    type: "SET_LOCALE",
-                    locale: e.currentTarget.value as Language
-                })
-            }
-        >
-            {Object.keys(Languages).map(x => {
-                const l = Languages[x as keyof typeof Languages];
-                return (
-                    <option value={x}>
-                        {l.emoji} {l.display}
-                    </option>
-                );
-            })}
-        </ComboBox>
-    );
+	return (
+		<ComboBox
+			value={props.locale}
+			onChange={(e) =>
+				dispatch({
+					type: "SET_LOCALE",
+					locale: e.currentTarget.value as Language,
+				})
+			}>
+			{Object.keys(Languages).map((x) => {
+				const l = Languages[x as keyof typeof Languages];
+				return (
+					<option value={x}>
+						{l.emoji} {l.display}
+					</option>
+				);
+			})}
+		</ComboBox>
+	);
 }
 
-export default connectState(
-    LocaleSelector,
-    state => {
-        return {
-            locale: state.locale
-        };
-    }
-);
+export default connectState(LocaleSelector, (state) => {
+	return {
+		locale: state.locale,
+	};
+});
diff --git a/src/components/common/ServerHeader.tsx b/src/components/common/ServerHeader.tsx
index 1218cdd1347755bdcb474dd928e37fbd6f599892..46e6cd5bad610b1860d902e2dedaf6708611b43d 100644
--- a/src/components/common/ServerHeader.tsx
+++ b/src/components/common/ServerHeader.tsx
@@ -1,40 +1,49 @@
-import Header from "../ui/Header";
-import styled from "styled-components";
-import { Link } from "react-router-dom";
-import IconButton from "../ui/IconButton";
 import { Cog } from "@styled-icons/boxicons-solid";
+import { Link } from "react-router-dom";
 import { Server } from "revolt.js/dist/api/objects";
 import { ServerPermission } from "revolt.js/dist/api/permissions";
+import styled from "styled-components";
+
 import { HookContext, useServerPermission } from "../../context/revoltjs/hooks";
 
+import Header from "../ui/Header";
+import IconButton from "../ui/IconButton";
+
 interface Props {
-    server: Server,
-    ctx: HookContext
+	server: Server;
+	ctx: HookContext;
 }
 
 const ServerName = styled.div`
-    flex-grow: 1;
+	flex-grow: 1;
 `;
 
 export default function ServerHeader({ server, ctx }: Props) {
-    const permissions = useServerPermission(server._id, ctx);
-    const bannerURL = ctx.client.servers.getBannerURL(server._id, { width: 480 }, true);
+	const permissions = useServerPermission(server._id, ctx);
+	const bannerURL = ctx.client.servers.getBannerURL(
+		server._id,
+		{ width: 480 },
+		true,
+	);
 
-    return (
-        <Header borders
-            placement="secondary"
-            background={typeof bannerURL !== 'undefined'}
-            style={{ background: bannerURL ? `url('${bannerURL}')` : undefined }}>
-            <ServerName>
-                { server.name }
-            </ServerName>
-            { (permissions & ServerPermission.ManageServer) > 0 && <div className="actions">
-                <Link to={`/server/${server._id}/settings`}>
-                    <IconButton>
-                        <Cog size={24} />
-                    </IconButton>
-                </Link>
-            </div> }
-        </Header>
-    )
+	return (
+		<Header
+			borders
+			placement="secondary"
+			background={typeof bannerURL !== "undefined"}
+			style={{
+				background: bannerURL ? `url('${bannerURL}')` : undefined,
+			}}>
+			<ServerName>{server.name}</ServerName>
+			{(permissions & ServerPermission.ManageServer) > 0 && (
+				<div className="actions">
+					<Link to={`/server/${server._id}/settings`}>
+						<IconButton>
+							<Cog size={24} />
+						</IconButton>
+					</Link>
+				</div>
+			)}
+		</Header>
+	);
 }
diff --git a/src/components/common/ServerIcon.tsx b/src/components/common/ServerIcon.tsx
index e3db726322a2054517af8653ab9b920dfa3c19ff..061a18d61ab0921860ebd394392f9aa674880603 100644
--- a/src/components/common/ServerIcon.tsx
+++ b/src/components/common/ServerIcon.tsx
@@ -1,47 +1,68 @@
+import { Server } from "revolt.js/dist/api/objects";
 import styled from "styled-components";
+
 import { useContext } from "preact/hooks";
-import { Server } from "revolt.js/dist/api/objects";
-import { IconBaseProps, ImageIconBase } from "./IconBase";
+
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 
+import { IconBaseProps, ImageIconBase } from "./IconBase";
+
 interface Props extends IconBaseProps<Server> {
-    server_name?: string;
+	server_name?: string;
 }
 
 const ServerText = styled.div`
-    display: grid;
-    padding: .2em;
-    overflow: hidden;
-    border-radius: 50%;
-    place-items: center;
-    color: var(--foreground);
-    background: var(--primary-background);
+	display: grid;
+	padding: 0.2em;
+	overflow: hidden;
+	border-radius: 50%;
+	place-items: center;
+	color: var(--foreground);
+	background: var(--primary-background);
 `;
 
-const fallback = '/assets/group.png';
-export default function ServerIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>) {
-    const client = useContext(AppContext);
-
-    const { target, attachment, size, animate, server_name, children, as, ...imgProps } = props;
-    const iconURL = client.generateFileURL(target?.icon ?? attachment, { max_side: 256 }, animate);
-
-    if (typeof iconURL === 'undefined') {
-        const name = target?.name ?? server_name ?? '';
-
-        return (
-            <ServerText style={{ width: size, height: size }}>
-                { name.split(' ')
-                      .map(x => x[0])
-                      .filter(x => typeof x !== 'undefined') }
-            </ServerText>
-        )
-    }
-
-    return (
-        <ImageIconBase {...imgProps}
-            width={size}
-            height={size}
-            aria-hidden="true"
-            src={iconURL} />
-    );
+const fallback = "/assets/group.png";
+export default function ServerIcon(
+	props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
+) {
+	const client = useContext(AppContext);
+
+	const {
+		target,
+		attachment,
+		size,
+		animate,
+		server_name,
+		children,
+		as,
+		...imgProps
+	} = props;
+	const iconURL = client.generateFileURL(
+		target?.icon ?? attachment,
+		{ max_side: 256 },
+		animate,
+	);
+
+	if (typeof iconURL === "undefined") {
+		const name = target?.name ?? server_name ?? "";
+
+		return (
+			<ServerText style={{ width: size, height: size }}>
+				{name
+					.split(" ")
+					.map((x) => x[0])
+					.filter((x) => typeof x !== "undefined")}
+			</ServerText>
+		);
+	}
+
+	return (
+		<ImageIconBase
+			{...imgProps}
+			width={size}
+			height={size}
+			aria-hidden="true"
+			src={iconURL}
+		/>
+	);
 }
diff --git a/src/components/common/Tooltip.tsx b/src/components/common/Tooltip.tsx
index 7a9957f4cc6355bf0d4a34d1f7ab3f4f8e35c6c3..2dbfa24f76279068928c2cd9f90b0986541cff28 100644
--- a/src/components/common/Tooltip.tsx
+++ b/src/components/common/Tooltip.tsx
@@ -1,49 +1,60 @@
-import { Text } from "preact-i18n";
+import Tippy, { TippyProps } from "@tippyjs/react";
 import styled from "styled-components";
+
+import { Text } from "preact-i18n";
+
 import { Children } from "../../types/Preact";
-import Tippy, { TippyProps } from '@tippyjs/react';
 
-type Props = Omit<TippyProps, 'children'> & {
-    children: Children;
-    content: Children;
-}
+type Props = Omit<TippyProps, "children"> & {
+	children: Children;
+	content: Children;
+};
 
 export default function Tooltip(props: Props) {
-    const { children, content, ...tippyProps } = props;
+	const { children, content, ...tippyProps } = props;
 
-    return (
-        <Tippy content={content} {...tippyProps}>
-            {/*
+	return (
+		<Tippy content={content} {...tippyProps}>
+			{/*
             // @ts-expect-error */}
-            <div>{ children }</div>
-        </Tippy>
-    );
+			<div>{children}</div>
+		</Tippy>
+	);
 }
 
 const PermissionTooltipBase = styled.div`
-    display: flex;
-    align-items: center;
-    flex-direction: column;
-    
-    span {
-        font-weight: 700;
-        text-transform: uppercase;
-        color: var(--secondary-foreground);
-        font-size: 11px;
-    }
-
-    code {
-        font-family: var(--monoscape-font);
-    }
+	display: flex;
+	align-items: center;
+	flex-direction: column;
+
+	span {
+		font-weight: 700;
+		text-transform: uppercase;
+		color: var(--secondary-foreground);
+		font-size: 11px;
+	}
+
+	code {
+		font-family: var(--monoscape-font);
+	}
 `;
 
-export function PermissionTooltip(props: Omit<Props, 'content'> & { permission: string }) {
-    const { permission, ...tooltipProps } = props;
+export function PermissionTooltip(
+	props: Omit<Props, "content"> & { permission: string },
+) {
+	const { permission, ...tooltipProps } = props;
 
-    return (
-        <Tooltip content={<PermissionTooltipBase>
-            <span><Text id="app.permissions.required" /></span>
-            <code>{ permission }</code>
-        </PermissionTooltipBase>} {...tooltipProps} />
-    )
+	return (
+		<Tooltip
+			content={
+				<PermissionTooltipBase>
+					<span>
+						<Text id="app.permissions.required" />
+					</span>
+					<code>{permission}</code>
+				</PermissionTooltipBase>
+			}
+			{...tooltipProps}
+		/>
+	);
 }
diff --git a/src/components/common/UpdateIndicator.tsx b/src/components/common/UpdateIndicator.tsx
index 937d6522a3004400bbc62ad67cdd693a4eb993f7..d818c2ee075da6f0312ad28daa39f0eb14ec52a1 100644
--- a/src/components/common/UpdateIndicator.tsx
+++ b/src/components/common/UpdateIndicator.tsx
@@ -1,26 +1,31 @@
-import { updateSW } from "../../main";
-import IconButton from "../ui/IconButton";
-import { ThemeContext } from "../../context/Theme";
 import { Download } from "@styled-icons/boxicons-regular";
-import { internalSubscribe } from "../../lib/eventEmitter";
+
 import { useContext, useEffect, useState } from "preact/hooks";
 
+import { internalSubscribe } from "../../lib/eventEmitter";
+
+import { ThemeContext } from "../../context/Theme";
+
+import IconButton from "../ui/IconButton";
+
+import { updateSW } from "../../main";
+
 var pendingUpdate = false;
-internalSubscribe('PWA', 'update', () => pendingUpdate = true);
+internalSubscribe("PWA", "update", () => (pendingUpdate = true));
 
 export default function UpdateIndicator() {
-    const [ pending, setPending ] = useState(pendingUpdate);
+	const [pending, setPending] = useState(pendingUpdate);
 
-    useEffect(() => {
-        return internalSubscribe('PWA', 'update', () => setPending(true));
-    });
+	useEffect(() => {
+		return internalSubscribe("PWA", "update", () => setPending(true));
+	});
 
-    if (!pending) return null;
-    const theme = useContext(ThemeContext);
+	if (!pending) return null;
+	const theme = useContext(ThemeContext);
 
-    return (
-        <IconButton onClick={() => updateSW(true)}>
-            <Download size={22} color={theme.success} />
-        </IconButton>
-    )
+	return (
+		<IconButton onClick={() => updateSW(true)}>
+			<Download size={22} color={theme.success} />
+		</IconButton>
+	);
 }
diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx
index f00aad634dec4f7e7b7151039dd636dca0c4b4c2..1dda28d9a683babb4a5bd124471a6458a8da8b02 100644
--- a/src/components/common/messaging/Message.tsx
+++ b/src/components/common/messaging/Message.tsx
@@ -1,78 +1,133 @@
-import Embed from "./embed/Embed";
-import UserIcon from "../user/UserIcon";
-import { Username } from "../user/UserShort";
-import Markdown from "../../markdown/Markdown";
-import { Children } from "../../../types/Preact";
-import Attachment from "./attachments/Attachment";
 import { attachContextMenu } from "preact-context-menu";
-import { useUser } from "../../../context/revoltjs/hooks";
+import { memo } from "preact/compat";
+import { useContext } from "preact/hooks";
+
 import { QueuedMessage } from "../../../redux/reducers/queue";
+
+import { useIntermediate } from "../../../context/intermediate/Intermediate";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import { useUser } from "../../../context/revoltjs/hooks";
 import { MessageObject } from "../../../context/revoltjs/util";
-import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./MessageBase";
+
 import Overline from "../../ui/Overline";
-import { useContext } from "preact/hooks";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
-import { memo } from "preact/compat";
+
+import { Children } from "../../../types/Preact";
+import Markdown from "../../markdown/Markdown";
+import UserIcon from "../user/UserIcon";
+import { Username } from "../user/UserShort";
+import MessageBase, {
+	MessageContent,
+	MessageDetail,
+	MessageInfo,
+} from "./MessageBase";
+import Attachment from "./attachments/Attachment";
 import { MessageReply } from "./attachments/MessageReply";
-import { useIntermediate } from "../../../context/intermediate/Intermediate";
+import Embed from "./embed/Embed";
 
 interface Props {
-    attachContext?: boolean
-    queued?: QueuedMessage
-    message: MessageObject
-    contrast?: boolean
-    content?: Children
-    head?: boolean
+	attachContext?: boolean;
+	queued?: QueuedMessage;
+	message: MessageObject;
+	contrast?: boolean;
+	content?: Children;
+	head?: boolean;
 }
 
-function Message({ attachContext, message, contrast, content: replacement, head: preferHead, queued }: Props) {
-    // TODO: Can improve re-renders here by providing a list
-    // TODO: of dependencies. We only need to update on u/avatar.
-    const user = useUser(message.author);
-    const client = useContext(AppContext);
-    const { openScreen } = useIntermediate();
+function Message({
+	attachContext,
+	message,
+	contrast,
+	content: replacement,
+	head: preferHead,
+	queued,
+}: Props) {
+	// TODO: Can improve re-renders here by providing a list
+	// TODO: of dependencies. We only need to update on u/avatar.
+	const user = useUser(message.author);
+	const client = useContext(AppContext);
+	const { openScreen } = useIntermediate();
 
-    const content = message.content as string;
-    const head = preferHead || (message.replies && message.replies.length > 0);
+	const content = message.content as string;
+	const head = preferHead || (message.replies && message.replies.length > 0);
 
-     // ! FIXME: tell fatal to make this type generic
-    // bree: Fatal please...
-    const userContext = attachContext 
-        ? attachContextMenu('Menu', { user: message.author, contextualChannel: message.channel }) as any
-        : undefined;
+	// ! FIXME: tell fatal to make this type generic
+	// bree: Fatal please...
+	const userContext = attachContext
+		? (attachContextMenu("Menu", {
+				user: message.author,
+				contextualChannel: message.channel,
+		  }) as any)
+		: undefined;
 
-    const openProfile = () => openScreen({ id: 'profile', user_id: message.author });
+	const openProfile = () =>
+		openScreen({ id: "profile", user_id: message.author });
 
-    return (
-        <div id={message._id}>
-            { message.replies?.map((message_id, index) => <MessageReply index={index} id={message_id} channel={message.channel} />) }
-            <MessageBase
-                head={head && !(message.replies && message.replies.length > 0)}
-                contrast={contrast}
-                sending={typeof queued !== 'undefined'}
-                mention={message.mentions?.includes(client.user!._id)}
-                failed={typeof queued?.error !== 'undefined'}
-                onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel, queued }) : undefined}>
-                <MessageInfo>
-                    { head ?
-                        <UserIcon target={user} size={36} onContextMenu={userContext} onClick={openProfile} /> :
-                        <MessageDetail message={message} position="left" /> }
-                </MessageInfo>
-                <MessageContent>
-                    { head && <span className="detail">
-                        <Username className="author" user={user} onContextMenu={userContext} onClick={openProfile} />
-                        <MessageDetail message={message} position="top" />
-                    </span> }
-                    { replacement ?? <Markdown content={content} /> }
-                    { queued?.error && <Overline type="error" error={queued.error} /> }
-                    { message.attachments?.map((attachment, index) =>
-                        <Attachment key={index} attachment={attachment} hasContent={ index > 0 || content.length > 0 } />) }
-                    { message.embeds?.map((embed, index) =>
-                        <Embed key={index} embed={embed} />) }
-                </MessageContent>
-            </MessageBase>
-        </div>
-    )
+	return (
+		<div id={message._id}>
+			{message.replies?.map((message_id, index) => (
+				<MessageReply
+					index={index}
+					id={message_id}
+					channel={message.channel}
+				/>
+			))}
+			<MessageBase
+				head={head && !(message.replies && message.replies.length > 0)}
+				contrast={contrast}
+				sending={typeof queued !== "undefined"}
+				mention={message.mentions?.includes(client.user!._id)}
+				failed={typeof queued?.error !== "undefined"}
+				onContextMenu={
+					attachContext
+						? attachContextMenu("Menu", {
+								message,
+								contextualChannel: message.channel,
+								queued,
+						  })
+						: undefined
+				}>
+				<MessageInfo>
+					{head ? (
+						<UserIcon
+							target={user}
+							size={36}
+							onContextMenu={userContext}
+							onClick={openProfile}
+						/>
+					) : (
+						<MessageDetail message={message} position="left" />
+					)}
+				</MessageInfo>
+				<MessageContent>
+					{head && (
+						<span className="detail">
+							<Username
+								className="author"
+								user={user}
+								onContextMenu={userContext}
+								onClick={openProfile}
+							/>
+							<MessageDetail message={message} position="top" />
+						</span>
+					)}
+					{replacement ?? <Markdown content={content} />}
+					{queued?.error && (
+						<Overline type="error" error={queued.error} />
+					)}
+					{message.attachments?.map((attachment, index) => (
+						<Attachment
+							key={index}
+							attachment={attachment}
+							hasContent={index > 0 || content.length > 0}
+						/>
+					))}
+					{message.embeds?.map((embed, index) => (
+						<Embed key={index} embed={embed} />
+					))}
+				</MessageContent>
+			</MessageBase>
+		</div>
+	);
 }
 
 export default memo(Message);
diff --git a/src/components/common/messaging/MessageBase.tsx b/src/components/common/messaging/MessageBase.tsx
index 6efa2284ef5da40c60de4394872de5e870899b22..ee9fa9466de8c37cc044471916fe2a18eea632af 100644
--- a/src/components/common/messaging/MessageBase.tsx
+++ b/src/components/common/messaging/MessageBase.tsx
@@ -1,189 +1,212 @@
 import dayjs from "dayjs";
-import Tooltip from "../Tooltip";
+import styled, { css } from "styled-components";
 import { decodeTime } from "ulid";
+
 import { Text } from "preact-i18n";
-import styled, { css } from "styled-components";
+
 import { MessageObject } from "../../../context/revoltjs/util";
 
+import Tooltip from "../Tooltip";
+
 export interface BaseMessageProps {
-    head?: boolean,
-    failed?: boolean,
-    mention?: boolean,
-    blocked?: boolean,
-    sending?: boolean,
-    contrast?: boolean
+	head?: boolean;
+	failed?: boolean;
+	mention?: boolean;
+	blocked?: boolean;
+	sending?: boolean;
+	contrast?: boolean;
 }
 
 export default styled.div<BaseMessageProps>`
-    display: flex;
-    overflow-x: none;
-    padding: .125rem;
-    flex-direction: row;
-    padding-right: 16px;
-
-    ${ props => props.contrast && css`
-        padding: .3rem;
-        border-radius: 4px;
-        background: var(--hover);
-    ` }
-
-    ${ props => props.head && css`
-        margin-top: 12px;
-    ` }
-
-    ${ props => props.mention && css`
-        background: var(--mention);
-    ` }
-
-    ${ props => props.blocked && css`
-        filter: blur(4px);
-        transition: 0.2s ease filter;
-
-        &:hover {
-            filter: none;
-        }
-    ` }
-
-    ${ props => props.sending && css`
-        opacity: 0.8;
-        color: var(--tertiary-foreground);
-    ` }
-
-    ${ props => props.failed && css`
-        color: var(--error);
-    ` }
+	display: flex;
+	overflow-x: none;
+	padding: 0.125rem;
+	flex-direction: row;
+	padding-right: 16px;
+
+	${(props) =>
+		props.contrast &&
+		css`
+			padding: 0.3rem;
+			border-radius: 4px;
+			background: var(--hover);
+		`}
+
+	${(props) =>
+		props.head &&
+		css`
+			margin-top: 12px;
+		`}
+
+    ${(props) =>
+		props.mention &&
+		css`
+			background: var(--mention);
+		`}
+
+    ${(props) =>
+		props.blocked &&
+		css`
+			filter: blur(4px);
+			transition: 0.2s ease filter;
+
+			&:hover {
+				filter: none;
+			}
+		`}
+
+    ${(props) =>
+		props.sending &&
+		css`
+			opacity: 0.8;
+			color: var(--tertiary-foreground);
+		`}
+
+    ${(props) =>
+		props.failed &&
+		css`
+			color: var(--error);
+		`}
 
     .detail {
-        gap: 8px;
-        display: flex;
-        align-items: center;
-    }
-
-    .author {
-        cursor: pointer;
-        font-weight: 600 !important;
-        
-        &:hover {
-            text-decoration: underline;
-        }
-    }
-    
-    .copy {
-        display: block;
-        overflow: hidden;
-    }
-
-    &:hover {
-        background: var(--hover);
-
-        time {
-            opacity: 1;
-        }
-    }
+		gap: 8px;
+		display: flex;
+		align-items: center;
+	}
+
+	.author {
+		cursor: pointer;
+		font-weight: 600 !important;
+
+		&:hover {
+			text-decoration: underline;
+		}
+	}
+
+	.copy {
+		display: block;
+		overflow: hidden;
+	}
+
+	&:hover {
+		background: var(--hover);
+
+		time {
+			opacity: 1;
+		}
+	}
 `;
 
 export const MessageInfo = styled.div`
-    width: 62px;
-    display: flex;
-    flex-shrink: 0;
-    padding-top: 2px;
-    flex-direction: row;
-    justify-content: center;
-
-    .copyBracket {
-        opacity: 0;
-        position: absolute;
-    }
-
-    .copyTime {
-        opacity: 0;
-        position: absolute;
-    }
-
-    svg {
-        user-select: none;
-        cursor: pointer;
-
-        &:active {
-            transform: translateY(1px);
-        }
-    }
-
-    time {
-        opacity: 0;
-    }
-
-    time, .edited {
-        margin-top: 1px;
-        cursor: default;
-        display: inline;
-        font-size: 10px;
-        color: var(--tertiary-foreground);
-    }
-
-    time, .edited > div {
-        &::selection {
-            background-color: transparent;
-            color: var(--tertiary-foreground);
-        }
-    }
+	width: 62px;
+	display: flex;
+	flex-shrink: 0;
+	padding-top: 2px;
+	flex-direction: row;
+	justify-content: center;
+
+	.copyBracket {
+		opacity: 0;
+		position: absolute;
+	}
+
+	.copyTime {
+		opacity: 0;
+		position: absolute;
+	}
+
+	svg {
+		user-select: none;
+		cursor: pointer;
+
+		&:active {
+			transform: translateY(1px);
+		}
+	}
+
+	time {
+		opacity: 0;
+	}
+
+	time,
+	.edited {
+		margin-top: 1px;
+		cursor: default;
+		display: inline;
+		font-size: 10px;
+		color: var(--tertiary-foreground);
+	}
+
+	time,
+	.edited > div {
+		&::selection {
+			background-color: transparent;
+			color: var(--tertiary-foreground);
+		}
+	}
 `;
 
 export const MessageContent = styled.div`
-    min-width: 0;
-    flex-grow: 1;
-    display: flex;
-    // overflow: hidden;
-    font-size: .875rem;
-    flex-direction: column;
-    justify-content: center;
+	min-width: 0;
+	flex-grow: 1;
+	display: flex;
+	// overflow: hidden;
+	font-size: 0.875rem;
+	flex-direction: column;
+	justify-content: center;
 `;
 
 export const DetailBase = styled.div`
-    gap: 4px;
-    font-size: 10px;
-    display: inline-flex;
-    color: var(--tertiary-foreground);
+	gap: 4px;
+	font-size: 10px;
+	display: inline-flex;
+	color: var(--tertiary-foreground);
 `;
 
-export function MessageDetail({ message, position }: { message: MessageObject, position: 'left' | 'top' }) {
-    if (position === 'left') {
-        if (message.edited) {
-            return (
-                <>
-                    <time className="copyTime">
-                        <i className="copyBracket">[</i>
-                        {dayjs(decodeTime(message._id)).format("H:mm")}
-                        <i className="copyBracket">]</i>
-                    </time>
-                    <span className="edited">
-                        <Tooltip content={dayjs(message.edited).format("LLLL")}>
-                            <Text id="app.main.channel.edited" />
-                        </Tooltip>
-                    </span>
-                </>
-            )
-        } else {
-            return (
-                <>
-                    <time>
-                        <i className="copyBracket">[</i>
-                        { dayjs(decodeTime(message._id)).format("H:mm") }
-                        <i className="copyBracket">]</i>
-                    </time>
-                </>
-            )
-        }
-    }
-
-    return (
-        <DetailBase>
-            <time>
-                {dayjs(decodeTime(message._id)).calendar()}
-            </time>
-            { message.edited && <Tooltip content={dayjs(message.edited).format("LLLL")}>
-                <Text id="app.main.channel.edited" />
-            </Tooltip> }
-        </DetailBase>
-    )
+export function MessageDetail({
+	message,
+	position,
+}: {
+	message: MessageObject;
+	position: "left" | "top";
+}) {
+	if (position === "left") {
+		if (message.edited) {
+			return (
+				<>
+					<time className="copyTime">
+						<i className="copyBracket">[</i>
+						{dayjs(decodeTime(message._id)).format("H:mm")}
+						<i className="copyBracket">]</i>
+					</time>
+					<span className="edited">
+						<Tooltip content={dayjs(message.edited).format("LLLL")}>
+							<Text id="app.main.channel.edited" />
+						</Tooltip>
+					</span>
+				</>
+			);
+		} else {
+			return (
+				<>
+					<time>
+						<i className="copyBracket">[</i>
+						{dayjs(decodeTime(message._id)).format("H:mm")}
+						<i className="copyBracket">]</i>
+					</time>
+				</>
+			);
+		}
+	}
+
+	return (
+		<DetailBase>
+			<time>{dayjs(decodeTime(message._id)).calendar()}</time>
+			{message.edited && (
+				<Tooltip content={dayjs(message.edited).format("LLLL")}>
+					<Text id="app.main.channel.edited" />
+				</Tooltip>
+			)}
+		</DetailBase>
+	);
 }
diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx
index 486f6f28a8da03f0477f3477cc7b95764aae47ff..a1e78ad72006820d208a872bb434390e7a3027d9 100644
--- a/src/components/common/messaging/MessageBox.tsx
+++ b/src/components/common/messaging/MessageBox.tsx
@@ -1,388 +1,496 @@
-import { ulid } from "ulid";
-import { Text } from "preact-i18n";
+import { ShieldX } from "@styled-icons/boxicons-regular";
+import { Send } from "@styled-icons/boxicons-solid";
+import Axios, { CancelTokenSource } from "axios";
 import { Channel } from "revolt.js";
+import { ChannelPermission } from "revolt.js/dist/api/permissions";
 import styled from "styled-components";
-import { dispatch } from "../../../redux";
-import { defer } from "../../../lib/defer";
-import IconButton from "../../ui/IconButton";
-import { PermissionTooltip } from "../Tooltip";
-import { Send } from '@styled-icons/boxicons-solid';
+import { ulid } from "ulid";
+
+import { Text } from "preact-i18n";
+import { useCallback, useContext, useEffect, useState } from "preact/hooks";
+
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
 import { debounce } from "../../../lib/debounce";
-import Axios, { CancelTokenSource } from "axios";
+import { defer } from "../../../lib/defer";
+import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
 import { useTranslation } from "../../../lib/i18n";
-import { Reply } from "../../../redux/reducers/queue";
+import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+import {
+	SingletonMessageRenderer,
+	SMOOTH_SCROLL_ON_RECEIVE,
+} from "../../../lib/renderer/Singleton";
+
+import { dispatch } from "../../../redux";
 import { connectState } from "../../../redux/connector";
+import { Reply } from "../../../redux/reducers/queue";
+
 import { SoundContext } from "../../../context/Settings";
-import { takeError } from "../../../context/revoltjs/util";
-import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
-import AutoComplete, { useAutoComplete } from "../AutoComplete";
-import { ChannelPermission } from "revolt.js/dist/api/permissions";
+import { useIntermediate } from "../../../context/intermediate/Intermediate";
+import {
+	FileUploader,
+	grabFiles,
+	uploadFile,
+} from "../../../context/revoltjs/FileUploads";
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 import { useChannelPermission } from "../../../context/revoltjs/hooks";
-import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
-import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
-import { useCallback, useContext, useEffect, useState } from "preact/hooks";
-import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { FileUploader, grabFiles, uploadFile } from "../../../context/revoltjs/FileUploads";
-import { SingletonMessageRenderer, SMOOTH_SCROLL_ON_RECEIVE } from "../../../lib/renderer/Singleton";
-import { ShieldX } from "@styled-icons/boxicons-regular";
+import { takeError } from "../../../context/revoltjs/util";
 
+import IconButton from "../../ui/IconButton";
+
+import AutoComplete, { useAutoComplete } from "../AutoComplete";
+import { PermissionTooltip } from "../Tooltip";
+import FilePreview from "./bars/FilePreview";
 import ReplyBar from "./bars/ReplyBar";
-import FilePreview from './bars/FilePreview';
 
 type Props = {
-    channel: Channel;
-    draft?: string;
+	channel: Channel;
+	draft?: string;
 };
 
 export type UploadState =
-    | { type: "none" }
-    | { type: "attached"; files: File[] }
-    | { type: "uploading"; files: File[]; percent: number; cancel: CancelTokenSource }
-    | { type: "sending"; files: File[] }
-    | { type: "failed"; files: File[]; error: string };
+	| { type: "none" }
+	| { type: "attached"; files: File[] }
+	| {
+			type: "uploading";
+			files: File[];
+			percent: number;
+			cancel: CancelTokenSource;
+	  }
+	| { type: "sending"; files: File[] }
+	| { type: "failed"; files: File[]; error: string };
 
 const Base = styled.div`
-    display: flex;
-    padding: 0 12px;
-    background: var(--message-box);
-
-    textarea {
-        font-size: .875rem;
-        background: transparent;
-    }
+	display: flex;
+	padding: 0 12px;
+	background: var(--message-box);
+
+	textarea {
+		font-size: 0.875rem;
+		background: transparent;
+	}
 `;
 
 const Blocked = styled.div`
-    display: flex;
-    align-items: center;
-    padding: 14px 0;
-    user-select: none;
-    font-size: .875rem;
-    color: var(--tertiary-foreground);
-
-    svg {
-        flex-shrink: 0;
-        margin-inline-end: 10px;
-    }
+	display: flex;
+	align-items: center;
+	padding: 14px 0;
+	user-select: none;
+	font-size: 0.875rem;
+	color: var(--tertiary-foreground);
+
+	svg {
+		flex-shrink: 0;
+		margin-inline-end: 10px;
+	}
 `;
 
 const Action = styled.div`
-    display: grid;
-    place-items: center;
+	display: grid;
+	place-items: center;
 `;
 
 // ! FIXME: add to app config and load from app config
 export const CAN_UPLOAD_AT_ONCE = 5;
 
 function MessageBox({ channel, draft }: Props) {
-    const [ uploadState, setUploadState ] = useState<UploadState>({ type: 'none' });
-    const [ typing, setTyping ] = useState<boolean | number>(false);
-    const [ replies, setReplies ] = useState<Reply[]>([]);
-    const playSound = useContext(SoundContext);
-    const { openScreen } = useIntermediate();
-    const client = useContext(AppContext);
-    const translate = useTranslation();
-
-    const permissions = useChannelPermission(channel._id);
-    if (!(permissions & ChannelPermission.SendMessage)) {
-        return (
-            <Base>
-                <Blocked>
-                    <PermissionTooltip permission="SendMessages" placement="top">
-                        <ShieldX size={22}/>
-                    </PermissionTooltip>
-                    <Text id="app.main.channel.misc.no_sending" />
-                </Blocked>
-            </Base>
-        )
-    }
-
-    function setMessage(content?: string) {
-        if (content) {
-            dispatch({
-                type: "SET_DRAFT",
-                channel: channel._id,
-                content
-            });
-        } else {
-            dispatch({
-                type: "CLEAR_DRAFT",
-                channel: channel._id
-            });
-        }
-    }
-
-    useEffect(() => {
-        function append(content: string, action: 'quote' | 'mention') {
-            const text =
-                action === "quote"
-                    ? `${content
-                          .split("\n")
-                          .map(x => `> ${x}`)
-                          .join("\n")}\n\n`
-                    : `${content} `;
-
-            if (!draft || draft.length === 0) {
-                setMessage(text);
-            } else {
-                setMessage(`${draft}\n${text}`);
-            }
-        }
-
-        return internalSubscribe("MessageBox", "append", append);
-    }, [ draft ]);
-
-    async function send() {
-        if (uploadState.type === 'uploading' || uploadState.type === 'sending') return;
-        
-        const content = draft?.trim() ?? '';
-        if (uploadState.type === 'attached') return sendFile(content);
-        if (content.length === 0) return;
-        
-        stopTyping();
-        setMessage();
-        setReplies([]);
-        playSound('outbound');
-
-        const nonce = ulid();
-        dispatch({
-            type: "QUEUE_ADD",
-            nonce,
-            channel: channel._id,
-            message: {
-                _id: nonce,
-                channel: channel._id,
-                author: client.user!._id,
-                
-                content,
-                replies
-            }
-        });
-
-        defer(() => SingletonMessageRenderer.jumpToBottom(channel._id, SMOOTH_SCROLL_ON_RECEIVE));
-
-        try {
-            await client.channels.sendMessage(channel._id, {
-                content,
-                nonce,
-                replies
-            });
-        } catch (error) {
-            dispatch({
-                type: "QUEUE_FAIL",
-                error: takeError(error),
-                nonce
-            });
-        }
-    }
-
-    async function sendFile(content: string) {
-        if (uploadState.type !== 'attached') return;
-        let attachments: string[] = [];
-
-        const cancel = Axios.CancelToken.source();
-        const files = uploadState.files;
-        stopTyping();
-        setUploadState({ type: "uploading", files, percent: 0, cancel });
-
-        try {
-            for (let i=0;i<files.length&&i<CAN_UPLOAD_AT_ONCE;i++) {
-                const file = files[i];
-                attachments.push(
-                    await uploadFile(client.configuration!.features.autumn.url, 'attachments', file, {
-                        onUploadProgress: e =>
-                        setUploadState({
-                            type: "uploading",
-                            files,
-                            percent: Math.round(((i * 100) + (100 * e.loaded) / e.total) / Math.min(files.length, CAN_UPLOAD_AT_ONCE)),
-                            cancel
-                        }),
-                        cancelToken: cancel.token
-                    })
-                );
-            }
-        } catch (err) {
-            if (err?.message === "cancel") {
-                setUploadState({
-                    type: "attached",
-                    files
-                });
-            } else {
-                setUploadState({
-                    type: "failed",
-                    files,
-                    error: takeError(err)
-                });
-            }
-
-            return;
-        }
-
-        setUploadState({
-            type: "sending",
-            files
-        });
-
-        const nonce = ulid();
-        try {
-            await client.channels.sendMessage(channel._id, {
-                content,
-                nonce,
-                replies,
-                attachments
-            });
-        } catch (err) {
-            setUploadState({
-                type: "failed",
-                files,
-                error: takeError(err)
-            });
-
-            return;
-        }
-
-        setMessage();
-        setReplies([]);
-        playSound('outbound');
-
-        if (files.length > CAN_UPLOAD_AT_ONCE) {
-            setUploadState({
-                type: "attached",
-                files: files.slice(CAN_UPLOAD_AT_ONCE)
-            });
-        } else {
-            setUploadState({ type: "none" });
-        }
-    }
-    
-    function startTyping() {
-        if (typeof typing === 'number' && + new Date() < typing) return;
-
-        const ws = client.websocket;
-        if (ws.connected) {
-            setTyping(+ new Date() + 4000);
-            ws.send({
-                type: "BeginTyping",
-                channel: channel._id
-            });
-        }
-    }
-
-    function stopTyping(force?: boolean) {
-        if (force || typing) {
-            const ws = client.websocket;
-            if (ws.connected) {
-                setTyping(false);
-                ws.send({
-                    type: "EndTyping",
-                    channel: channel._id
-                });
-            }
-        }
-    }
-
-    const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [ channel._id ]);
-    const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } =
-        useAutoComplete(setMessage, {
-            users: { type: 'channel', id: channel._id },
-            channels: channel.channel_type === 'TextChannel' ? { server: channel.server } : undefined
-        });
-
-    return (
-        <>
-            <AutoComplete {...autoCompleteProps} />
-            <FilePreview state={uploadState} addFile={() => uploadState.type === 'attached' &&
-                grabFiles(20_000_000, files => setUploadState({ type: 'attached', files: [ ...uploadState.files, ...files ] }),
-                    () => openScreen({ id: "error", error: "FileTooLarge" }), true)}
-                removeFile={index => {
-                    if (uploadState.type !== 'attached') return;
-                    if (uploadState.files.length === 1) {
-                        setUploadState({ type: 'none' });
-                    } else {
-                        setUploadState({ type: 'attached', files: uploadState.files.filter((_, i) => index !== i) });
-                    }
-                }} />
-            <ReplyBar channel={channel._id} replies={replies} setReplies={setReplies} />
-            <Base>
-                { (permissions & ChannelPermission.UploadFiles) ? <Action>
-                    <FileUploader
-                        size={24}
-                        behaviour='multi'
-                        style='attachment'
-                        fileType='attachments'
-                        maxFileSize={20_000_000}
-
-                        attached={uploadState.type !== 'none'}
-                        uploading={uploadState.type === 'uploading' || uploadState.type === 'sending'}
-
-                        remove={async () => setUploadState({ type: "none" })}
-                        onChange={files => setUploadState({ type: "attached", files })}
-                        cancel={() => uploadState.type === 'uploading' && uploadState.cancel.cancel("cancel")}
-                        append={files => {
-                            if (files.length === 0) return;
-
-                            if (uploadState.type === 'none') {
-                                setUploadState({ type: 'attached', files });
-                            } else if (uploadState.type === 'attached') {
-                                setUploadState({ type: 'attached', files: [ ...uploadState.files, ...files ] });
-                            }
-                        }}
-                    />
-                </Action> : undefined }
-                <TextAreaAutoSize
-                    autoFocus
-                    hideBorder
-                    maxRows={5}
-                    padding={14}
-                    id="message"
-                    value={draft ?? ''}
-                    onKeyUp={onKeyUp}
-                    onKeyDown={e => {
-                        if (onKeyDown(e)) return;
-
-                        if (
-                            e.key === "ArrowUp" &&
-                            (!draft || draft.length === 0)
-                        ) {
-                            e.preventDefault();
-                            internalEmit("MessageRenderer", "edit_last");
-                            return;
-                        }
-
-                        if (!e.shiftKey && e.key === "Enter" && !isTouchscreenDevice) {
-                            e.preventDefault();
-                            return send();
-                        }
-                        
-                        debouncedStopTyping(true);
-                    }}
-                    placeholder={
-                        channel.channel_type === "DirectMessage" ? translate("app.main.channel.message_who", {
-                            person: client.users.get(client.channels.getRecipient(channel._id))?.username })
-                        : channel.channel_type === "SavedMessages" ? translate("app.main.channel.message_saved")
-                        : translate("app.main.channel.message_where", { channel_name: channel.name })
-                    }
-                    disabled={uploadState.type === 'uploading' || uploadState.type === 'sending'}
-                    onChange={e => {
-                        setMessage(e.currentTarget.value);
-                        startTyping();
-                        onChange(e);
-                    }}
-                    onFocus={onFocus}
-                    onBlur={onBlur} />
-                { isTouchscreenDevice && <Action>
-                    <IconButton onClick={send}>
-                        <Send size={20} />
-                    </IconButton>
-                </Action> }
-            </Base>
-        </>
-    )
+	const [uploadState, setUploadState] = useState<UploadState>({
+		type: "none",
+	});
+	const [typing, setTyping] = useState<boolean | number>(false);
+	const [replies, setReplies] = useState<Reply[]>([]);
+	const playSound = useContext(SoundContext);
+	const { openScreen } = useIntermediate();
+	const client = useContext(AppContext);
+	const translate = useTranslation();
+
+	const permissions = useChannelPermission(channel._id);
+	if (!(permissions & ChannelPermission.SendMessage)) {
+		return (
+			<Base>
+				<Blocked>
+					<PermissionTooltip
+						permission="SendMessages"
+						placement="top">
+						<ShieldX size={22} />
+					</PermissionTooltip>
+					<Text id="app.main.channel.misc.no_sending" />
+				</Blocked>
+			</Base>
+		);
+	}
+
+	function setMessage(content?: string) {
+		if (content) {
+			dispatch({
+				type: "SET_DRAFT",
+				channel: channel._id,
+				content,
+			});
+		} else {
+			dispatch({
+				type: "CLEAR_DRAFT",
+				channel: channel._id,
+			});
+		}
+	}
+
+	useEffect(() => {
+		function append(content: string, action: "quote" | "mention") {
+			const text =
+				action === "quote"
+					? `${content
+							.split("\n")
+							.map((x) => `> ${x}`)
+							.join("\n")}\n\n`
+					: `${content} `;
+
+			if (!draft || draft.length === 0) {
+				setMessage(text);
+			} else {
+				setMessage(`${draft}\n${text}`);
+			}
+		}
+
+		return internalSubscribe("MessageBox", "append", append);
+	}, [draft]);
+
+	async function send() {
+		if (uploadState.type === "uploading" || uploadState.type === "sending")
+			return;
+
+		const content = draft?.trim() ?? "";
+		if (uploadState.type === "attached") return sendFile(content);
+		if (content.length === 0) return;
+
+		stopTyping();
+		setMessage();
+		setReplies([]);
+		playSound("outbound");
+
+		const nonce = ulid();
+		dispatch({
+			type: "QUEUE_ADD",
+			nonce,
+			channel: channel._id,
+			message: {
+				_id: nonce,
+				channel: channel._id,
+				author: client.user!._id,
+
+				content,
+				replies,
+			},
+		});
+
+		defer(() =>
+			SingletonMessageRenderer.jumpToBottom(
+				channel._id,
+				SMOOTH_SCROLL_ON_RECEIVE,
+			),
+		);
+
+		try {
+			await client.channels.sendMessage(channel._id, {
+				content,
+				nonce,
+				replies,
+			});
+		} catch (error) {
+			dispatch({
+				type: "QUEUE_FAIL",
+				error: takeError(error),
+				nonce,
+			});
+		}
+	}
+
+	async function sendFile(content: string) {
+		if (uploadState.type !== "attached") return;
+		let attachments: string[] = [];
+
+		const cancel = Axios.CancelToken.source();
+		const files = uploadState.files;
+		stopTyping();
+		setUploadState({ type: "uploading", files, percent: 0, cancel });
+
+		try {
+			for (let i = 0; i < files.length && i < CAN_UPLOAD_AT_ONCE; i++) {
+				const file = files[i];
+				attachments.push(
+					await uploadFile(
+						client.configuration!.features.autumn.url,
+						"attachments",
+						file,
+						{
+							onUploadProgress: (e) =>
+								setUploadState({
+									type: "uploading",
+									files,
+									percent: Math.round(
+										(i * 100 + (100 * e.loaded) / e.total) /
+											Math.min(
+												files.length,
+												CAN_UPLOAD_AT_ONCE,
+											),
+									),
+									cancel,
+								}),
+							cancelToken: cancel.token,
+						},
+					),
+				);
+			}
+		} catch (err) {
+			if (err?.message === "cancel") {
+				setUploadState({
+					type: "attached",
+					files,
+				});
+			} else {
+				setUploadState({
+					type: "failed",
+					files,
+					error: takeError(err),
+				});
+			}
+
+			return;
+		}
+
+		setUploadState({
+			type: "sending",
+			files,
+		});
+
+		const nonce = ulid();
+		try {
+			await client.channels.sendMessage(channel._id, {
+				content,
+				nonce,
+				replies,
+				attachments,
+			});
+		} catch (err) {
+			setUploadState({
+				type: "failed",
+				files,
+				error: takeError(err),
+			});
+
+			return;
+		}
+
+		setMessage();
+		setReplies([]);
+		playSound("outbound");
+
+		if (files.length > CAN_UPLOAD_AT_ONCE) {
+			setUploadState({
+				type: "attached",
+				files: files.slice(CAN_UPLOAD_AT_ONCE),
+			});
+		} else {
+			setUploadState({ type: "none" });
+		}
+	}
+
+	function startTyping() {
+		if (typeof typing === "number" && +new Date() < typing) return;
+
+		const ws = client.websocket;
+		if (ws.connected) {
+			setTyping(+new Date() + 4000);
+			ws.send({
+				type: "BeginTyping",
+				channel: channel._id,
+			});
+		}
+	}
+
+	function stopTyping(force?: boolean) {
+		if (force || typing) {
+			const ws = client.websocket;
+			if (ws.connected) {
+				setTyping(false);
+				ws.send({
+					type: "EndTyping",
+					channel: channel._id,
+				});
+			}
+		}
+	}
+
+	const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [
+		channel._id,
+	]);
+	const {
+		onChange,
+		onKeyUp,
+		onKeyDown,
+		onFocus,
+		onBlur,
+		...autoCompleteProps
+	} = useAutoComplete(setMessage, {
+		users: { type: "channel", id: channel._id },
+		channels:
+			channel.channel_type === "TextChannel"
+				? { server: channel.server }
+				: undefined,
+	});
+
+	return (
+		<>
+			<AutoComplete {...autoCompleteProps} />
+			<FilePreview
+				state={uploadState}
+				addFile={() =>
+					uploadState.type === "attached" &&
+					grabFiles(
+						20_000_000,
+						(files) =>
+							setUploadState({
+								type: "attached",
+								files: [...uploadState.files, ...files],
+							}),
+						() =>
+							openScreen({ id: "error", error: "FileTooLarge" }),
+						true,
+					)
+				}
+				removeFile={(index) => {
+					if (uploadState.type !== "attached") return;
+					if (uploadState.files.length === 1) {
+						setUploadState({ type: "none" });
+					} else {
+						setUploadState({
+							type: "attached",
+							files: uploadState.files.filter(
+								(_, i) => index !== i,
+							),
+						});
+					}
+				}}
+			/>
+			<ReplyBar
+				channel={channel._id}
+				replies={replies}
+				setReplies={setReplies}
+			/>
+			<Base>
+				{permissions & ChannelPermission.UploadFiles ? (
+					<Action>
+						<FileUploader
+							size={24}
+							behaviour="multi"
+							style="attachment"
+							fileType="attachments"
+							maxFileSize={20_000_000}
+							attached={uploadState.type !== "none"}
+							uploading={
+								uploadState.type === "uploading" ||
+								uploadState.type === "sending"
+							}
+							remove={async () =>
+								setUploadState({ type: "none" })
+							}
+							onChange={(files) =>
+								setUploadState({ type: "attached", files })
+							}
+							cancel={() =>
+								uploadState.type === "uploading" &&
+								uploadState.cancel.cancel("cancel")
+							}
+							append={(files) => {
+								if (files.length === 0) return;
+
+								if (uploadState.type === "none") {
+									setUploadState({ type: "attached", files });
+								} else if (uploadState.type === "attached") {
+									setUploadState({
+										type: "attached",
+										files: [...uploadState.files, ...files],
+									});
+								}
+							}}
+						/>
+					</Action>
+				) : undefined}
+				<TextAreaAutoSize
+					autoFocus
+					hideBorder
+					maxRows={5}
+					padding={14}
+					id="message"
+					value={draft ?? ""}
+					onKeyUp={onKeyUp}
+					onKeyDown={(e) => {
+						if (onKeyDown(e)) return;
+
+						if (
+							e.key === "ArrowUp" &&
+							(!draft || draft.length === 0)
+						) {
+							e.preventDefault();
+							internalEmit("MessageRenderer", "edit_last");
+							return;
+						}
+
+						if (
+							!e.shiftKey &&
+							e.key === "Enter" &&
+							!isTouchscreenDevice
+						) {
+							e.preventDefault();
+							return send();
+						}
+
+						debouncedStopTyping(true);
+					}}
+					placeholder={
+						channel.channel_type === "DirectMessage"
+							? translate("app.main.channel.message_who", {
+									person: client.users.get(
+										client.channels.getRecipient(
+											channel._id,
+										),
+									)?.username,
+							  })
+							: channel.channel_type === "SavedMessages"
+							? translate("app.main.channel.message_saved")
+							: translate("app.main.channel.message_where", {
+									channel_name: channel.name,
+							  })
+					}
+					disabled={
+						uploadState.type === "uploading" ||
+						uploadState.type === "sending"
+					}
+					onChange={(e) => {
+						setMessage(e.currentTarget.value);
+						startTyping();
+						onChange(e);
+					}}
+					onFocus={onFocus}
+					onBlur={onBlur}
+				/>
+				{isTouchscreenDevice && (
+					<Action>
+						<IconButton onClick={send}>
+							<Send size={20} />
+						</IconButton>
+					</Action>
+				)}
+			</Base>
+		</>
+	);
 }
 
-export default connectState<Omit<Props, "dispatch" | "draft">>(MessageBox, (state, { channel }) => {
-    return {
-        draft: state.drafts[channel._id]
-    }
-}, true)
+export default connectState<Omit<Props, "dispatch" | "draft">>(
+	MessageBox,
+	(state, { channel }) => {
+		return {
+			draft: state.drafts[channel._id],
+		};
+	},
+	true,
+);
diff --git a/src/components/common/messaging/SystemMessage.tsx b/src/components/common/messaging/SystemMessage.tsx
index e239076d71c2fd1dae4338a14ee6081f6e2dbeac..ecd78dc5938589488a681acb95d1a837089057bc 100644
--- a/src/components/common/messaging/SystemMessage.tsx
+++ b/src/components/common/messaging/SystemMessage.tsx
@@ -1,149 +1,160 @@
 import { User } from "revolt.js";
 import styled from "styled-components";
-import UserShort from "../user/UserShort";
-import { TextReact } from "../../../lib/i18n";
+
 import { attachContextMenu } from "preact-context-menu";
+
+import { TextReact } from "../../../lib/i18n";
+
+import { useForceUpdate, useUser } from "../../../context/revoltjs/hooks";
 import { MessageObject } from "../../../context/revoltjs/util";
+
+import UserShort from "../user/UserShort";
 import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
-import { useForceUpdate, useUser } from "../../../context/revoltjs/hooks";
 
 const SystemContent = styled.div`
-    gap: 4px;
-    display: flex;
-    padding: 2px 0;
-    flex-wrap: wrap;
-    align-items: center;
-    flex-direction: row;
+	gap: 4px;
+	display: flex;
+	padding: 2px 0;
+	flex-wrap: wrap;
+	align-items: center;
+	flex-direction: row;
 `;
 
 type SystemMessageParsed =
-    | { type: "text"; content: string }
-    | { type: "user_added"; user: User; by: User }
-    | { type: "user_remove"; user: User; by: User }
-    | { type: "user_joined"; user: User }
-    | { type: "user_left"; user: User }
-    | { type: "user_kicked"; user: User }
-    | { type: "user_banned"; user: User }
-    | { type: "channel_renamed"; name: string; by: User }
-    | { type: "channel_description_changed"; by: User }
-    | { type: "channel_icon_changed"; by: User };
+	| { type: "text"; content: string }
+	| { type: "user_added"; user: User; by: User }
+	| { type: "user_remove"; user: User; by: User }
+	| { type: "user_joined"; user: User }
+	| { type: "user_left"; user: User }
+	| { type: "user_kicked"; user: User }
+	| { type: "user_banned"; user: User }
+	| { type: "channel_renamed"; name: string; by: User }
+	| { type: "channel_description_changed"; by: User }
+	| { type: "channel_icon_changed"; by: User };
 
 interface Props {
-    attachContext?: boolean;
-    message: MessageObject;
+	attachContext?: boolean;
+	message: MessageObject;
 }
 
 export function SystemMessage({ attachContext, message }: Props) {
-    const ctx = useForceUpdate();
+	const ctx = useForceUpdate();
 
-    let data: SystemMessageParsed;
-    let content = message.content;
-    if (typeof content === "object") {
-        switch (content.type) {
-            case "text":
-                data = content;
-                break;
-            case "user_added":
-            case "user_remove":
-                data = {
-                    type: content.type,
-                    user: useUser(content.id, ctx) as User,
-                    by: useUser(content.by, ctx) as User
-                };
-                break;
-            case "user_joined":
-            case "user_left":
-            case "user_kicked":
-            case "user_banned":
-                data = {
-                    type: content.type,
-                    user: useUser(content.id, ctx) as User
-                };
-                break;
-            case "channel_renamed":
-                data = {
-                    type: "channel_renamed",
-                    name: content.name,
-                    by: useUser(content.by, ctx) as User
-                };
-                break;
-            case "channel_description_changed":
-            case "channel_icon_changed":
-                data = {
-                    type: content.type,
-                    by: useUser(content.by, ctx) as User
-                };
-                break;
-            default:
-                data = { type: "text", content: JSON.stringify(content) };
-        }
-    } else {
-        data = { type: "text", content };
-    }
+	let data: SystemMessageParsed;
+	let content = message.content;
+	if (typeof content === "object") {
+		switch (content.type) {
+			case "text":
+				data = content;
+				break;
+			case "user_added":
+			case "user_remove":
+				data = {
+					type: content.type,
+					user: useUser(content.id, ctx) as User,
+					by: useUser(content.by, ctx) as User,
+				};
+				break;
+			case "user_joined":
+			case "user_left":
+			case "user_kicked":
+			case "user_banned":
+				data = {
+					type: content.type,
+					user: useUser(content.id, ctx) as User,
+				};
+				break;
+			case "channel_renamed":
+				data = {
+					type: "channel_renamed",
+					name: content.name,
+					by: useUser(content.by, ctx) as User,
+				};
+				break;
+			case "channel_description_changed":
+			case "channel_icon_changed":
+				data = {
+					type: content.type,
+					by: useUser(content.by, ctx) as User,
+				};
+				break;
+			default:
+				data = { type: "text", content: JSON.stringify(content) };
+		}
+	} else {
+		data = { type: "text", content };
+	}
 
-    let children;
-    switch (data.type) {
-        case "text":
-            children = <span>{data.content}</span>;
-            break;
-        case "user_added":
-        case "user_remove":
-            children = (
-                <TextReact
-                    id={`app.main.channel.system.${data.type === 'user_added' ? "added_by" : "removed_by"}`}
-                    fields={{
-                        user: <UserShort user={data.user} />,
-                        other_user: <UserShort user={data.by} />
-                    }}
-                />
-            );
-            break;
-        case "user_joined":
-        case "user_left":
-        case "user_kicked":
-        case "user_banned":
-            children = (
-                <TextReact
-                    id={`app.main.channel.system.${data.type}`}
-                    fields={{
-                        user: <UserShort user={data.user} />
-                    }}
-                />
-            );
-            break;
-        case "channel_renamed":
-            children = (
-                <TextReact
-                    id={`app.main.channel.system.channel_renamed`}
-                    fields={{
-                        user: <UserShort user={data.by} />,
-                        name: <b>{data.name}</b>
-                    }}
-                />
-            );
-            break;
-        case "channel_description_changed":
-        case "channel_icon_changed":
-            children = (
-                <TextReact
-                    id={`app.main.channel.system.${data.type}`}
-                    fields={{
-                        user: <UserShort user={data.by} />
-                    }}
-                />
-            );
-            break;
-    }
+	let children;
+	switch (data.type) {
+		case "text":
+			children = <span>{data.content}</span>;
+			break;
+		case "user_added":
+		case "user_remove":
+			children = (
+				<TextReact
+					id={`app.main.channel.system.${
+						data.type === "user_added" ? "added_by" : "removed_by"
+					}`}
+					fields={{
+						user: <UserShort user={data.user} />,
+						other_user: <UserShort user={data.by} />,
+					}}
+				/>
+			);
+			break;
+		case "user_joined":
+		case "user_left":
+		case "user_kicked":
+		case "user_banned":
+			children = (
+				<TextReact
+					id={`app.main.channel.system.${data.type}`}
+					fields={{
+						user: <UserShort user={data.user} />,
+					}}
+				/>
+			);
+			break;
+		case "channel_renamed":
+			children = (
+				<TextReact
+					id={`app.main.channel.system.channel_renamed`}
+					fields={{
+						user: <UserShort user={data.by} />,
+						name: <b>{data.name}</b>,
+					}}
+				/>
+			);
+			break;
+		case "channel_description_changed":
+		case "channel_icon_changed":
+			children = (
+				<TextReact
+					id={`app.main.channel.system.${data.type}`}
+					fields={{
+						user: <UserShort user={data.by} />,
+					}}
+				/>
+			);
+			break;
+	}
 
-    return (
-        <MessageBase
-            onContextMenu={attachContext ? attachContextMenu('Menu',
-                { message, contextualChannel: message.channel }
-            ) : undefined}>
-            <MessageInfo>
-                <MessageDetail message={message} position="left" />
-            </MessageInfo>
-            <SystemContent>{children}</SystemContent>
-        </MessageBase>
-    );
+	return (
+		<MessageBase
+			onContextMenu={
+				attachContext
+					? attachContextMenu("Menu", {
+							message,
+							contextualChannel: message.channel,
+					  })
+					: undefined
+			}>
+			<MessageInfo>
+				<MessageDetail message={message} position="left" />
+			</MessageInfo>
+			<SystemContent>{children}</SystemContent>
+		</MessageBase>
+	);
 }
diff --git a/src/components/common/messaging/attachments/Attachment.tsx b/src/components/common/messaging/attachments/Attachment.tsx
index 2ef979a2c8c22b29d0fbbd835cb3aad8357f44df..4f88feb7aa693b790fdfdc14bc0d5d0d8af6abbe 100644
--- a/src/components/common/messaging/attachments/Attachment.tsx
+++ b/src/components/common/messaging/attachments/Attachment.tsx
@@ -1,126 +1,133 @@
-import TextFile from "./TextFile";
-import { Text } from "preact-i18n";
-import classNames from "classnames";
+import { Attachment as AttachmentRJS } from "revolt.js/dist/api/objects";
+
 import styles from "./Attachment.module.scss";
-import AttachmentActions from "./AttachmentActions";
+import classNames from "classnames";
+import { Text } from "preact-i18n";
 import { useContext, useState } from "preact/hooks";
-import { AppContext } from "../../../../context/revoltjs/RevoltClient";
-import { Attachment as AttachmentRJS } from "revolt.js/dist/api/objects";
+
 import { useIntermediate } from "../../../../context/intermediate/Intermediate";
+import { AppContext } from "../../../../context/revoltjs/RevoltClient";
+
 import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
+import AttachmentActions from "./AttachmentActions";
+import TextFile from "./TextFile";
 
 interface Props {
-    attachment: AttachmentRJS;
-    hasContent: boolean;
+	attachment: AttachmentRJS;
+	hasContent: boolean;
 }
 
 const MAX_ATTACHMENT_WIDTH = 480;
 
 export default function Attachment({ attachment, hasContent }: Props) {
-    const client = useContext(AppContext);
-    const { openScreen } = useIntermediate();
-    const { filename, metadata } = attachment;
-    const [ spoiler, setSpoiler ] = useState(filename.startsWith("SPOILER_"));
-    const [ loaded, setLoaded ] = useState(false)
-
-    const url = client.generateFileURL(attachment, { width: MAX_ATTACHMENT_WIDTH * 1.5 }, true);
+	const client = useContext(AppContext);
+	const { openScreen } = useIntermediate();
+	const { filename, metadata } = attachment;
+	const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
+	const [loaded, setLoaded] = useState(false);
 
+	const url = client.generateFileURL(
+		attachment,
+		{ width: MAX_ATTACHMENT_WIDTH * 1.5 },
+		true,
+	);
 
-    switch (metadata.type) {
-        case "Image": {
-            return (
-                <div
-                    className={styles.container}
-                    onClick={() => spoiler && setSpoiler(false)}
-                >
-                    {spoiler && (
-                        <div className={styles.overflow}>
-                            <span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
-                        </div>
-                    )}
-                    <img
-                        src={url}
-                        alt={filename}
-                        width={metadata.width}
-                        height={metadata.height}
-                        data-spoiler={spoiler}
-                        data-has-content={hasContent}
-                        className={classNames(styles.attachment, styles.image, loaded && styles.loaded)}
-                        onClick={() =>
-                            openScreen({ id: "image_viewer", attachment })
-                        }
-                        onMouseDown={ev =>
-                            ev.button === 1 &&
-                            window.open(url, "_blank")
-                        }
-                        onLoad={() => setLoaded(true)}
-                    />
-                </div>
-            );
-        }
-        case "Audio": {
-            return (
-                <div
-                    className={classNames(styles.attachment, styles.audio)}
-                    data-has-content={hasContent}
-                >
-                    <AttachmentActions attachment={attachment} />
-                    <audio src={url} controls />
-                </div>
-            );
-        }
-        case "Video": {
-            return (
-                <div
-                    className={styles.container}
-                    onClick={() => spoiler && setSpoiler(false)}>
-                    {spoiler && (
-                        <div className={styles.overflow}>
-                            <span><Text id="app.main.channel.misc.spoiler_attachment" /></span>
-                        </div>
-                    )}
-                    <div
-                        data-spoiler={spoiler}
-                        data-has-content={hasContent}
-                        className={classNames(styles.attachment, styles.video)}
-                    >
-                        <AttachmentActions attachment={attachment} />
-                        <video
-                            src={url}
-                            width={metadata.width}
-                            height={metadata.height}
-                            className={classNames(loaded && styles.loaded)}
-                            controls
-                            onMouseDown={ev =>
-                                ev.button === 1 &&
-                                window.open(url, "_blank")
-                            }
-                            onLoadedMetadata={() => setLoaded(true)}
-                        />
-                    </div>
-                </div>
-            );
-        }
-        case 'Text': {
-            return (
-                <div
-                    className={classNames(styles.attachment, styles.text)}
-                    data-has-content={hasContent}
-                >
-                    <TextFile attachment={attachment} />
-                    <AttachmentActions attachment={attachment} />
-                </div>
-            );
-        }
-        default: {
-            return (
-                <div
-                    className={classNames(styles.attachment, styles.file)}
-                    data-has-content={hasContent}
-                >
-                    <AttachmentActions attachment={attachment} />
-                </div>
-            );
-        }
-    }
+	switch (metadata.type) {
+		case "Image": {
+			return (
+				<div
+					className={styles.container}
+					onClick={() => spoiler && setSpoiler(false)}>
+					{spoiler && (
+						<div className={styles.overflow}>
+							<span>
+								<Text id="app.main.channel.misc.spoiler_attachment" />
+							</span>
+						</div>
+					)}
+					<img
+						src={url}
+						alt={filename}
+						width={metadata.width}
+						height={metadata.height}
+						data-spoiler={spoiler}
+						data-has-content={hasContent}
+						className={classNames(
+							styles.attachment,
+							styles.image,
+							loaded && styles.loaded,
+						)}
+						onClick={() =>
+							openScreen({ id: "image_viewer", attachment })
+						}
+						onMouseDown={(ev) =>
+							ev.button === 1 && window.open(url, "_blank")
+						}
+						onLoad={() => setLoaded(true)}
+					/>
+				</div>
+			);
+		}
+		case "Audio": {
+			return (
+				<div
+					className={classNames(styles.attachment, styles.audio)}
+					data-has-content={hasContent}>
+					<AttachmentActions attachment={attachment} />
+					<audio src={url} controls />
+				</div>
+			);
+		}
+		case "Video": {
+			return (
+				<div
+					className={styles.container}
+					onClick={() => spoiler && setSpoiler(false)}>
+					{spoiler && (
+						<div className={styles.overflow}>
+							<span>
+								<Text id="app.main.channel.misc.spoiler_attachment" />
+							</span>
+						</div>
+					)}
+					<div
+						data-spoiler={spoiler}
+						data-has-content={hasContent}
+						className={classNames(styles.attachment, styles.video)}>
+						<AttachmentActions attachment={attachment} />
+						<video
+							src={url}
+							width={metadata.width}
+							height={metadata.height}
+							className={classNames(loaded && styles.loaded)}
+							controls
+							onMouseDown={(ev) =>
+								ev.button === 1 && window.open(url, "_blank")
+							}
+							onLoadedMetadata={() => setLoaded(true)}
+						/>
+					</div>
+				</div>
+			);
+		}
+		case "Text": {
+			return (
+				<div
+					className={classNames(styles.attachment, styles.text)}
+					data-has-content={hasContent}>
+					<TextFile attachment={attachment} />
+					<AttachmentActions attachment={attachment} />
+				</div>
+			);
+		}
+		default: {
+			return (
+				<div
+					className={classNames(styles.attachment, styles.file)}
+					data-has-content={hasContent}>
+					<AttachmentActions attachment={attachment} />
+				</div>
+			);
+		}
+	}
 }
diff --git a/src/components/common/messaging/attachments/AttachmentActions.tsx b/src/components/common/messaging/attachments/AttachmentActions.tsx
index 8b297f4becb000814764698ea314b911d53e7186..b5458c9cf6e923c1c2e32e08159f52003a948386 100644
--- a/src/components/common/messaging/attachments/AttachmentActions.tsx
+++ b/src/components/common/messaging/attachments/AttachmentActions.tsx
@@ -1,83 +1,115 @@
-import { useContext } from 'preact/hooks';
-import styles from './Attachment.module.scss';
-import IconButton from '../../../ui/IconButton';
+import {
+	Download,
+	LinkExternal,
+	File,
+	Headphone,
+	Video,
+} from "@styled-icons/boxicons-regular";
 import { Attachment } from "revolt.js/dist/api/objects";
-import { determineFileSize } from '../../../../lib/fileSize';
-import { AppContext } from '../../../../context/revoltjs/RevoltClient';
-import { Download, LinkExternal, File, Headphone, Video } from '@styled-icons/boxicons-regular';
-import classNames from 'classnames';
+
+import styles from "./Attachment.module.scss";
+import classNames from "classnames";
+import { useContext } from "preact/hooks";
+
+import { determineFileSize } from "../../../../lib/fileSize";
+
+import { AppContext } from "../../../../context/revoltjs/RevoltClient";
+
+import IconButton from "../../../ui/IconButton";
 
 interface Props {
-    attachment: Attachment;
+	attachment: Attachment;
 }
 
 export default function AttachmentActions({ attachment }: Props) {
-    const client = useContext(AppContext);
-    const { filename, metadata, size } = attachment;
+	const client = useContext(AppContext);
+	const { filename, metadata, size } = attachment;
 
-    const url = client.generateFileURL(attachment)!;
-    const open_url = `${url}/${filename}`;
-    const download_url = url.replace('attachments', 'attachments/download')
+	const url = client.generateFileURL(attachment)!;
+	const open_url = `${url}/${filename}`;
+	const download_url = url.replace("attachments", "attachments/download");
 
-    // for some reason revolt.js says the size is a string even though it's a number
-    const filesize = determineFileSize(size);
+	const filesize = determineFileSize(size);
 
-    switch (metadata.type) {
-        case 'Image':
-            return (
-                <div className={classNames(styles.actions, styles.imageAction)}>
-                    <span className={styles.filename}>{filename}</span>
-                    <span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
-                    <a href={open_url} target="_blank" className={styles.iconType} >
-                        <IconButton>
-                            <LinkExternal size={24} />
-                        </IconButton>
-                    </a>
-                    <a href={download_url} className={styles.downloadIcon} download target="_blank">
-                        <IconButton>
-                            <Download size={24} />
-                        </IconButton>
-                    </a>
-                </div>
-            )
-        case 'Audio':
-            return (
-                <div className={classNames(styles.actions, styles.audioAction)}>
-                    <Headphone size={24} className={styles.iconType} />
-                    <span className={styles.filename}>{filename}</span>
-                    <span className={styles.filesize}>{filesize}</span>
-                    <a href={download_url} className={styles.downloadIcon} download target="_blank">
-                        <IconButton>
-                            <Download size={24} />
-                        </IconButton>
-                    </a>
-                </div>
-            )
-        case 'Video':
-            return (
-                <div className={classNames(styles.actions, styles.videoAction)}>
-                    <Video size={24} className={styles.iconType} />
-                    <span className={styles.filename}>{filename}</span>
-                    <span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
-                    <a href={download_url} className={styles.downloadIcon} download target="_blank">
-                        <IconButton>
-                            <Download size={24} />
-                        </IconButton>
-                    </a>
-                </div>
-            )
-        default:
-            return (
-                <div className={styles.actions}>
-                    <File size={24} className={styles.iconType} />
-                    <span className={styles.filename}>{filename}</span>
-                    <span className={styles.filesize}>{filesize}</span>
-                    <a href={download_url} className={styles.downloadIcon} download target="_blank">
-                        <IconButton>
-                            <Download size={24} />
-                        </IconButton>
-                    </a>
-                </div>
-            )
-    }
+	switch (metadata.type) {
+		case "Image":
+			return (
+				<div className={classNames(styles.actions, styles.imageAction)}>
+					<span className={styles.filename}>{filename}</span>
+					<span className={styles.filesize}>
+						{metadata.width + "x" + metadata.height} ({filesize})
+					</span>
+					<a
+						href={open_url}
+						target="_blank"
+						className={styles.iconType}>
+						<IconButton>
+							<LinkExternal size={24} />
+						</IconButton>
+					</a>
+					<a
+						href={download_url}
+						className={styles.downloadIcon}
+						download
+						target="_blank">
+						<IconButton>
+							<Download size={24} />
+						</IconButton>
+					</a>
+				</div>
+			);
+		case "Audio":
+			return (
+				<div className={classNames(styles.actions, styles.audioAction)}>
+					<Headphone size={24} className={styles.iconType} />
+					<span className={styles.filename}>{filename}</span>
+					<span className={styles.filesize}>{filesize}</span>
+					<a
+						href={download_url}
+						className={styles.downloadIcon}
+						download
+						target="_blank">
+						<IconButton>
+							<Download size={24} />
+						</IconButton>
+					</a>
+				</div>
+			);
+		case "Video":
+			return (
+				<div className={classNames(styles.actions, styles.videoAction)}>
+					<Video size={24} className={styles.iconType} />
+					<span className={styles.filename}>{filename}</span>
+					<span className={styles.filesize}>
+						{metadata.width + "x" + metadata.height} ({filesize})
+					</span>
+					<a
+						href={download_url}
+						className={styles.downloadIcon}
+						download
+						target="_blank">
+						<IconButton>
+							<Download size={24} />
+						</IconButton>
+					</a>
+				</div>
+			);
+		default:
+			return (
+				<div className={styles.actions}>
+					<File size={24} className={styles.iconType} />
+					<span className={styles.filename}>{filename}</span>
+					<span className={styles.filesize}>{filesize}</span>
+					<a
+						href={download_url}
+						className={styles.downloadIcon}
+						download
+						target="_blank">
+						<IconButton>
+							<Download size={24} />
+						</IconButton>
+					</a>
+				</div>
+			);
+	}
 }
diff --git a/src/components/common/messaging/attachments/MessageReply.tsx b/src/components/common/messaging/attachments/MessageReply.tsx
index 95d931f01a95ba3a3299f7c605b9999636f0a11d..4a28383df134568f1e4a46cd1c82ca4f20d1ccfb 100644
--- a/src/components/common/messaging/attachments/MessageReply.tsx
+++ b/src/components/common/messaging/attachments/MessageReply.tsx
@@ -1,72 +1,93 @@
-import { Text } from "preact-i18n";
-import UserShort from "../../user/UserShort";
-import styled, { css } from "styled-components";
-import Markdown from "../../../markdown/Markdown";
 import { Reply, File } from "@styled-icons/boxicons-regular";
-import { useUser } from "../../../../context/revoltjs/hooks";
+import styled, { css } from "styled-components";
+
+import { Text } from "preact-i18n";
+
 import { useRenderState } from "../../../../lib/renderer/Singleton";
 
+import { useUser } from "../../../../context/revoltjs/hooks";
+
+import Markdown from "../../../markdown/Markdown";
+import UserShort from "../../user/UserShort";
+
 interface Props {
-    channel: string
-    index: number
-    id: string
+	channel: string;
+	index: number;
+	id: string;
 }
 
-export const ReplyBase = styled.div<{ head?: boolean, fail?: boolean, preview?: boolean }>`
-    gap: 4px;
-    display: flex;
-    font-size: 0.8em;
-    margin-left: 30px;
-    user-select: none;
-    margin-bottom: 4px;
-    align-items: center;
-    color: var(--secondary-foreground);
+export const ReplyBase = styled.div<{
+	head?: boolean;
+	fail?: boolean;
+	preview?: boolean;
+}>`
+	gap: 4px;
+	display: flex;
+	font-size: 0.8em;
+	margin-left: 30px;
+	user-select: none;
+	margin-bottom: 4px;
+	align-items: center;
+	color: var(--secondary-foreground);
 
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
 
-    svg:first-child {
-        flex-shrink: 0;
-        transform: scaleX(-1);
-        color: var(--tertiary-foreground);
-    }
+	svg:first-child {
+		flex-shrink: 0;
+		transform: scaleX(-1);
+		color: var(--tertiary-foreground);
+	}
 
-    ${ props => props.fail && css`
-        color: var(--tertiary-foreground);
-    ` }
+	${(props) =>
+		props.fail &&
+		css`
+			color: var(--tertiary-foreground);
+		`}
 
-    ${ props => props.head && css`
-        margin-top: 12px;
-    ` }
+	${(props) =>
+		props.head &&
+		css`
+			margin-top: 12px;
+		`}
 
-    ${ props => props.preview && css`
-        margin-left: 0;
-    ` }
+    ${(props) =>
+		props.preview &&
+		css`
+			margin-left: 0;
+		`}
 `;
 
 export function MessageReply({ index, channel, id }: Props) {
-    const view = useRenderState(channel);
-    if (view?.type !== 'RENDER') return null;
+	const view = useRenderState(channel);
+	if (view?.type !== "RENDER") return null;
 
-    const message = view.messages.find(x => x._id === id);
-    if (!message) {
-        return (
-            <ReplyBase head={index === 0} fail>
-                <Reply size={16} />
-                <span><Text id="app.main.channel.misc.failed_load" /></span>
-            </ReplyBase>
-        )
-    }
+	const message = view.messages.find((x) => x._id === id);
+	if (!message) {
+		return (
+			<ReplyBase head={index === 0} fail>
+				<Reply size={16} />
+				<span>
+					<Text id="app.main.channel.misc.failed_load" />
+				</span>
+			</ReplyBase>
+		);
+	}
 
-    const user = useUser(message.author);
+	const user = useUser(message.author);
 
-    return (
-        <ReplyBase head={index === 0}>
-            <Reply size={16} />
-            <UserShort user={user} size={16} />
-            { message.attachments && message.attachments.length > 0 && <File size={16} /> }
-            <Markdown disallowBigEmoji content={(message.content as string).replace(/\n/g, ' ')} />
-        </ReplyBase>
-    )
+	return (
+		<ReplyBase head={index === 0}>
+			<Reply size={16} />
+			<UserShort user={user} size={16} />
+			{message.attachments && message.attachments.length > 0 && (
+				<File size={16} />
+			)}
+			<Markdown
+				disallowBigEmoji
+				content={(message.content as string).replace(/\n/g, " ")}
+			/>
+		</ReplyBase>
+	);
 }
diff --git a/src/components/common/messaging/attachments/TextFile.tsx b/src/components/common/messaging/attachments/TextFile.tsx
index c01976f566eafdce373b322ddb9856b497d4f5c1..a31d3d99d2a45c015c2511ccbcacbd9e32846425 100644
--- a/src/components/common/messaging/attachments/TextFile.tsx
+++ b/src/components/common/messaging/attachments/TextFile.tsx
@@ -1,57 +1,72 @@
-import axios from 'axios';
-import Preloader from '../../../ui/Preloader';
-import styles from './Attachment.module.scss';
-import { Attachment } from 'revolt.js/dist/api/objects';
-import { useContext, useEffect, useState } from 'preact/hooks';
-import RequiresOnline from '../../../../context/revoltjs/RequiresOnline';
-import { AppContext, StatusContext } from '../../../../context/revoltjs/RevoltClient';
+import axios from "axios";
+import { Attachment } from "revolt.js/dist/api/objects";
+
+import styles from "./Attachment.module.scss";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
+import {
+	AppContext,
+	StatusContext,
+} from "../../../../context/revoltjs/RevoltClient";
+
+import Preloader from "../../../ui/Preloader";
 
 interface Props {
-    attachment: Attachment;
+	attachment: Attachment;
 }
 
 const fileCache: { [key: string]: string } = {};
 
 export default function TextFile({ attachment }: Props) {
-    const [ content, setContent ] = useState<undefined | string>(undefined);
-    const [ loading, setLoading ] = useState(false);
-    const status = useContext(StatusContext);
-    const client = useContext(AppContext);
-
-    const url = client.generateFileURL(attachment)!;
-
-    useEffect(() => {
-        if (typeof content !== 'undefined') return;
-        if (loading) return;
-        setLoading(true);
-
-        let cached = fileCache[attachment._id];
-        if (cached) {
-            setContent(cached);
-            setLoading(false);
-        } else {
-            axios.get(url)
-                .then(res => {
-                    setContent(res.data);
-                    fileCache[attachment._id] = res.data;
-                    setLoading(false);
-                })
-                .catch(() => {
-                    console.error("Failed to load text file. [", attachment._id, "]");
-                    setLoading(false)
-                })
-        }
-    }, [ content, loading, status ]);
-
-    return (
-        <div className={styles.textContent} data-loading={typeof content === 'undefined'}>
-            {
-                content ?
-                    <pre><code>{ content }</code></pre>
-                    : <RequiresOnline>
-                        <Preloader type="ring" />
-                    </RequiresOnline>
-            }
-        </div>
-    )
+	const [content, setContent] = useState<undefined | string>(undefined);
+	const [loading, setLoading] = useState(false);
+	const status = useContext(StatusContext);
+	const client = useContext(AppContext);
+
+	const url = client.generateFileURL(attachment)!;
+
+	useEffect(() => {
+		if (typeof content !== "undefined") return;
+		if (loading) return;
+		setLoading(true);
+
+		let cached = fileCache[attachment._id];
+		if (cached) {
+			setContent(cached);
+			setLoading(false);
+		} else {
+			axios
+				.get(url)
+				.then((res) => {
+					setContent(res.data);
+					fileCache[attachment._id] = res.data;
+					setLoading(false);
+				})
+				.catch(() => {
+					console.error(
+						"Failed to load text file. [",
+						attachment._id,
+						"]",
+					);
+					setLoading(false);
+				});
+		}
+	}, [content, loading, status]);
+
+	return (
+		<div
+			className={styles.textContent}
+			data-loading={typeof content === "undefined"}>
+			{content ? (
+				<pre>
+					<code>{content}</code>
+				</pre>
+			) : (
+				<RequiresOnline>
+					<Preloader type="ring" />
+				</RequiresOnline>
+			)}
+		</div>
+	);
 }
diff --git a/src/components/common/messaging/bars/FilePreview.tsx b/src/components/common/messaging/bars/FilePreview.tsx
index 80f9fae594cb8f04aad16e8a1b42710b61322e23..92137d92f9619c437c7f99082d99495dba5ed8e7 100644
--- a/src/components/common/messaging/bars/FilePreview.tsx
+++ b/src/components/common/messaging/bars/FilePreview.tsx
@@ -1,194 +1,233 @@
-import { Text } from "preact-i18n";
+import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
 import styled from "styled-components";
+
+import { Text } from "preact-i18n";
+import { useEffect, useState } from "preact/hooks";
+
+import { determineFileSize } from "../../../../lib/fileSize";
+
 import { CAN_UPLOAD_AT_ONCE, UploadState } from "../MessageBox";
-import { useEffect, useState } from 'preact/hooks';
-import { determineFileSize } from '../../../../lib/fileSize';
-import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
 
 interface Props {
-    state: UploadState,
-    addFile: () => void,
-    removeFile: (index: number) => void
+	state: UploadState;
+	addFile: () => void;
+	removeFile: (index: number) => void;
 }
 
 const Container = styled.div`
-    gap: 4px;
-    padding: 8px;
-    display: flex;
-    user-select: none;
-    flex-direction: column;
-    background: var(--message-box);
+	gap: 4px;
+	padding: 8px;
+	display: flex;
+	user-select: none;
+	flex-direction: column;
+	background: var(--message-box);
 `;
 
 const Carousel = styled.div`
-    gap: 8px;
-    display: flex;
-    overflow-x: scroll;
-    flex-direction: row;
+	gap: 8px;
+	display: flex;
+	overflow-x: scroll;
+	flex-direction: row;
 `;
 
 const Entry = styled.div`
-    display: flex;
-    flex-direction: column;
-
-    &.fade {
-        opacity: 0.4;
-    }
-
-    span.fn {
-        margin: auto;
-        font-size: .8em;
-        overflow: hidden;
-        max-width: 180px;
-        text-align: center;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-        color: var(--secondary-foreground);
-    }
-
-    span.size {
-        font-size: .6em;
-        color: var(--tertiary-foreground);
-        text-align: center;
-    }
+	display: flex;
+	flex-direction: column;
+
+	&.fade {
+		opacity: 0.4;
+	}
+
+	span.fn {
+		margin: auto;
+		font-size: 0.8em;
+		overflow: hidden;
+		max-width: 180px;
+		text-align: center;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+		color: var(--secondary-foreground);
+	}
+
+	span.size {
+		font-size: 0.6em;
+		color: var(--tertiary-foreground);
+		text-align: center;
+	}
 `;
 
 const Description = styled.div`
-    gap: 4px;
-    display: flex;
-    font-size: 0.9em;
-    align-items: center;
-    color: var(--secondary-foreground);
+	gap: 4px;
+	display: flex;
+	font-size: 0.9em;
+	align-items: center;
+	color: var(--secondary-foreground);
 `;
 
 const Divider = styled.div`
-    width: 4px;
-    height: 130px;
-    flex-shrink: 0;
-    border-radius: 4px;
-    background: var(--tertiary-background);
+	width: 4px;
+	height: 130px;
+	flex-shrink: 0;
+	border-radius: 4px;
+	background: var(--tertiary-background);
 `;
 
 const EmptyEntry = styled.div`
-    width: 100px;
-    height: 100px;
-    display: grid;
-    flex-shrink: 0;
-    cursor: pointer;
-    border-radius: 4px;
-    place-items: center;
-    background: var(--primary-background);
-    transition: 0.1s ease background-color;
-
-    &:hover {
-        background: var(--secondary-background);
-    }
+	width: 100px;
+	height: 100px;
+	display: grid;
+	flex-shrink: 0;
+	cursor: pointer;
+	border-radius: 4px;
+	place-items: center;
+	background: var(--primary-background);
+	transition: 0.1s ease background-color;
+
+	&:hover {
+		background: var(--secondary-background);
+	}
 `;
 
 const PreviewBox = styled.div`
-    display: grid;
-    grid-template: "main" 100px / minmax(100px, 1fr);
-    justify-items: center;
-    
-    background: var(--primary-background);
-
-    overflow: hidden;
-    
-    cursor: pointer;
-    border-radius: 4px;
-    
-    .icon, .overlay { grid-area: main }
-
-    .icon {
-        height: 100px;
-        width: 100%;
-        margin-bottom: 4px;
-        object-fit: contain;
-    }
-
-    .overlay {
-        display: grid;
-        align-items: center;
-        justify-content: center;
-
-        width: 100%;
-        height: 100%;
-        
-        opacity: 0;
-        visibility: hidden;
-
-        transition: 0.1s ease opacity;
-    }
-
-    &:hover {
-        .overlay {
-            visibility: visible;
-            opacity: 1;
-            background-color: rgba(0, 0, 0, 0.8);
-        }
-    }
-` 
-
-function FileEntry({ file, remove, index }: { file: File, remove?: () => void, index: number }) {
-    if (!file.type.startsWith('image/')) return (
-        <Entry className={index >= CAN_UPLOAD_AT_ONCE ? 'fade' : ''}>
-            <PreviewBox onClick={remove}>
-                <EmptyEntry className="icon">
-                    <File size={36} />
-                </EmptyEntry>
-                <div class="overlay"><XCircle size={36} /></div>
-            </PreviewBox>
-            <span class="fn">{file.name}</span>
-            <span class="size">{determineFileSize(file.size)}</span>
-        </Entry>
-    );
-
-    const [ url, setURL ] = useState('');
-
-    useEffect(() => {
-        let url: string = URL.createObjectURL(file);
-        setURL(url);
-        return () => URL.revokeObjectURL(url);
-    }, [ file ]);
-
-    return (
-        <Entry className={index >= CAN_UPLOAD_AT_ONCE ? 'fade' : ''}>
-            <PreviewBox onClick={remove}>
-                <img class="icon" src={url} alt={file.name} />
-                <div class="overlay"><XCircle size={36} /></div>
-            </PreviewBox>
-            <span class="fn">{file.name}</span>
-            <span class="size">{determineFileSize(file.size)}</span>
-        </Entry>
-    )
+	display: grid;
+	grid-template: "main" 100px / minmax(100px, 1fr);
+	justify-items: center;
+
+	background: var(--primary-background);
+
+	overflow: hidden;
+
+	cursor: pointer;
+	border-radius: 4px;
+
+	.icon,
+	.overlay {
+		grid-area: main;
+	}
+
+	.icon {
+		height: 100px;
+		width: 100%;
+		margin-bottom: 4px;
+		object-fit: contain;
+	}
+
+	.overlay {
+		display: grid;
+		align-items: center;
+		justify-content: center;
+
+		width: 100%;
+		height: 100%;
+
+		opacity: 0;
+		visibility: hidden;
+
+		transition: 0.1s ease opacity;
+	}
+
+	&:hover {
+		.overlay {
+			visibility: visible;
+			opacity: 1;
+			background-color: rgba(0, 0, 0, 0.8);
+		}
+	}
+`;
+
+function FileEntry({
+	file,
+	remove,
+	index,
+}: {
+	file: File;
+	remove?: () => void;
+	index: number;
+}) {
+	if (!file.type.startsWith("image/"))
+		return (
+			<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
+				<PreviewBox onClick={remove}>
+					<EmptyEntry className="icon">
+						<File size={36} />
+					</EmptyEntry>
+					<div class="overlay">
+						<XCircle size={36} />
+					</div>
+				</PreviewBox>
+				<span class="fn">{file.name}</span>
+				<span class="size">{determineFileSize(file.size)}</span>
+			</Entry>
+		);
+
+	const [url, setURL] = useState("");
+
+	useEffect(() => {
+		let url: string = URL.createObjectURL(file);
+		setURL(url);
+		return () => URL.revokeObjectURL(url);
+	}, [file]);
+
+	return (
+		<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
+			<PreviewBox onClick={remove}>
+				<img class="icon" src={url} alt={file.name} />
+				<div class="overlay">
+					<XCircle size={36} />
+				</div>
+			</PreviewBox>
+			<span class="fn">{file.name}</span>
+			<span class="size">{determineFileSize(file.size)}</span>
+		</Entry>
+	);
 }
 
 export default function FilePreview({ state, addFile, removeFile }: Props) {
-    if (state.type === 'none') return null;
-
-    return (
-        <Container>
-            <Carousel>
-                { state.files.map((file, index) =>
-                    <>
-                        { index === CAN_UPLOAD_AT_ONCE && <Divider /> }
-                        <FileEntry index={index} file={file} key={file.name} remove={state.type === 'attached' ? () => removeFile(index) : undefined} />
-                    </>
-                ) }
-                { state.type === 'attached' && <EmptyEntry onClick={addFile}><Plus size={48} /></EmptyEntry> }
-            </Carousel>
-            { state.type === 'uploading' && <Description>
-                <Share size={24} />
-                <Text id="app.main.channel.uploading_file" /> ({state.percent}%)
-            </Description> }
-            { state.type === 'sending' && <Description>
-                <Share size={24} />
-                Sending...
-            </Description> }
-            { state.type === 'failed' && <Description>
-                <X size={24} />
-                <Text id={`error.${state.error}`} />
-            </Description> }
-        </Container>
-    );
+	if (state.type === "none") return null;
+
+	return (
+		<Container>
+			<Carousel>
+				{state.files.map((file, index) => (
+					<>
+						{index === CAN_UPLOAD_AT_ONCE && <Divider />}
+						<FileEntry
+							index={index}
+							file={file}
+							key={file.name}
+							remove={
+								state.type === "attached"
+									? () => removeFile(index)
+									: undefined
+							}
+						/>
+					</>
+				))}
+				{state.type === "attached" && (
+					<EmptyEntry onClick={addFile}>
+						<Plus size={48} />
+					</EmptyEntry>
+				)}
+			</Carousel>
+			{state.type === "uploading" && (
+				<Description>
+					<Share size={24} />
+					<Text id="app.main.channel.uploading_file" /> (
+					{state.percent}%)
+				</Description>
+			)}
+			{state.type === "sending" && (
+				<Description>
+					<Share size={24} />
+					Sending...
+				</Description>
+			)}
+			{state.type === "failed" && (
+				<Description>
+					<X size={24} />
+					<Text id={`error.${state.error}`} />
+				</Description>
+			)}
+		</Container>
+	);
 }
diff --git a/src/components/common/messaging/bars/JumpToBottom.tsx b/src/components/common/messaging/bars/JumpToBottom.tsx
index ff0f527879595554c7cae8131a9cae17375400e7..9accc90e88947004c4c29ecc281133c3bea6de02 100644
--- a/src/components/common/messaging/bars/JumpToBottom.tsx
+++ b/src/components/common/messaging/bars/JumpToBottom.tsx
@@ -1,53 +1,64 @@
-import { Text } from "preact-i18n";
-import styled from "styled-components";
 import { DownArrow } from "@styled-icons/boxicons-regular";
-import { SingletonMessageRenderer, useRenderState } from "../../../../lib/renderer/Singleton";
+import styled from "styled-components";
+
+import { Text } from "preact-i18n";
+
+import {
+	SingletonMessageRenderer,
+	useRenderState,
+} from "../../../../lib/renderer/Singleton";
 
 const Bar = styled.div`
-    z-index: 10;
-    position: relative;
-
-    > div {
-        top: -26px;
-        width: 100%;
-        position: absolute;
-        border-radius: 4px 4px 0 0;
-        display: flex;
-        cursor: pointer;
-        font-size: 13px;
-        padding: 4px 8px;
-        user-select: none;
-        color: var(--secondary-foreground);
-        background: var(--secondary-background);
-        justify-content: space-between;
-        transition: color ease-in-out .08s;
-
-        > div {
-            display: flex;
-            align-items: center;
-            gap: 6px;
-        }
-
-        &:hover {
-            color: var(--primary-text);
-        }
-
-        &:active {
-            transform: translateY(1px);
-        }
-    }
+	z-index: 10;
+	position: relative;
+
+	> div {
+		top: -26px;
+		width: 100%;
+		position: absolute;
+		border-radius: 4px 4px 0 0;
+		display: flex;
+		cursor: pointer;
+		font-size: 13px;
+		padding: 4px 8px;
+		user-select: none;
+		color: var(--secondary-foreground);
+		background: var(--secondary-background);
+		justify-content: space-between;
+		transition: color ease-in-out 0.08s;
+
+		> div {
+			display: flex;
+			align-items: center;
+			gap: 6px;
+		}
+
+		&:hover {
+			color: var(--primary-text);
+		}
+
+		&:active {
+			transform: translateY(1px);
+		}
+	}
 `;
 
 export default function JumpToBottom({ id }: { id: string }) {
-    const view = useRenderState(id);
-    if (!view || view.type !== 'RENDER' || view.atBottom) return null;
-
-    return (
-        <Bar>
-            <div onClick={() => SingletonMessageRenderer.jumpToBottom(id, true)}>
-                <div><Text id="app.main.channel.misc.viewing_old" /></div>
-                <div><Text id="app.main.channel.misc.jump_present" /> <DownArrow size={18}/></div>
-            </div>
-        </Bar>
-    )
+	const view = useRenderState(id);
+	if (!view || view.type !== "RENDER" || view.atBottom) return null;
+
+	return (
+		<Bar>
+			<div
+				onClick={() => SingletonMessageRenderer.jumpToBottom(id, true)}>
+				<div>
+					<Text id="app.main.channel.misc.viewing_old" />
+				</div>
+				<div>
+					<Text id="app.main.channel.misc.jump_present" />{" "}
+					<DownArrow size={18} />
+				</div>
+			</div>
+		</Bar>
+	);
 }
diff --git a/src/components/common/messaging/bars/ReplyBar.tsx b/src/components/common/messaging/bars/ReplyBar.tsx
index 7ef6417399c327c56b16bd3a5d848e7c43fd7d8b..5c4dbdada2452d6afc6ce2b0ac7c19e5d9429c19 100644
--- a/src/components/common/messaging/bars/ReplyBar.tsx
+++ b/src/components/common/messaging/bars/ReplyBar.tsx
@@ -1,94 +1,141 @@
-import { Text } from "preact-i18n";
+import {
+	At,
+	Reply as ReplyIcon,
+	File,
+	XCircle,
+} from "@styled-icons/boxicons-regular";
 import styled from "styled-components";
-import UserShort from "../../user/UserShort";
-import IconButton from "../../../ui/IconButton";
-import Markdown from "../../../markdown/Markdown";
+
+import { Text } from "preact-i18n";
 import { StateUpdater, useEffect } from "preact/hooks";
-import { ReplyBase } from "../attachments/MessageReply";
-import { Reply } from "../../../../redux/reducers/queue";
-import { useUsers } from "../../../../context/revoltjs/hooks";
+
 import { internalSubscribe } from "../../../../lib/eventEmitter";
 import { useRenderState } from "../../../../lib/renderer/Singleton";
-import { At, Reply as ReplyIcon, File, XCircle } from "@styled-icons/boxicons-regular";
+
+import { Reply } from "../../../../redux/reducers/queue";
+
+import { useUsers } from "../../../../context/revoltjs/hooks";
+
+import IconButton from "../../../ui/IconButton";
+
+import Markdown from "../../../markdown/Markdown";
+import UserShort from "../../user/UserShort";
+import { ReplyBase } from "../attachments/MessageReply";
 
 interface Props {
-    channel: string,
-    replies: Reply[],
-    setReplies: StateUpdater<Reply[]>
+	channel: string;
+	replies: Reply[];
+	setReplies: StateUpdater<Reply[]>;
 }
 
 const Base = styled.div`
-    display: flex;
-    padding: 0 22px;
-    user-select: none;
-    align-items: center;
-    background: var(--message-box);
-
-    div {
-        flex-grow: 1;
-    }
-
-    .actions {
-        gap: 12px;
-        display: flex;
-    }
-    
-    .toggle {
-        gap: 4px;
-        display: flex;
-        font-size: 0.7em;
-        align-items: center;
-    }
+	display: flex;
+	padding: 0 22px;
+	user-select: none;
+	align-items: center;
+	background: var(--message-box);
+
+	div {
+		flex-grow: 1;
+	}
+
+	.actions {
+		gap: 12px;
+		display: flex;
+	}
+
+	.toggle {
+		gap: 4px;
+		display: flex;
+		font-size: 0.7em;
+		align-items: center;
+	}
 `;
 
 // ! FIXME: Move to global config
 const MAX_REPLIES = 5;
 export default function ReplyBar({ channel, replies, setReplies }: Props) {
-    useEffect(() => {
-        return internalSubscribe("ReplyBar", "add", id => replies.length < MAX_REPLIES && !replies.find(x => x.id === id) && setReplies([ ...replies, { id, mention: false } ]));
-    }, [ replies ]);
-
-    const view = useRenderState(channel);
-    if (view?.type !== 'RENDER') return null;
-
-    const ids = replies.map(x => x.id);
-    const messages = view.messages.filter(x => ids.includes(x._id));
-    const users = useUsers(messages.map(x => x.author));
-    
-    return (
-        <div>
-            { replies.map((reply, index) => {
-                let message = messages.find(x => reply.id === x._id);
-                // ! FIXME: better solution would be to
-                // ! have a hook for resolving messages from 
-                // ! render state along with relevant users
-                // -> which then fetches any unknown messages
-                if (!message) return <span><Text id="app.main.channel.misc.failed_load" /></span>;
-
-                let user = users.find(x => message!.author === x?._id);
-                if (!user) return;
-
-                return (
-                    <Base key={reply.id}>
-                        <ReplyBase preview>
-                            <ReplyIcon size={22} />
-                            <UserShort user={user} size={16} />
-                            { message.attachments && message.attachments.length > 0 && <File size={16} /> }
-                            <Markdown disallowBigEmoji content={(message.content as string).replace(/\n/g, ' ')} />
-                        </ReplyBase>
-                        <span class="actions">
-                            <IconButton onClick={() => setReplies(replies.map((_, i) => i === index ? { ..._, mention: !_.mention } : _))}>
-                                <span class="toggle">
-                                    <At size={16} /> { reply.mention ? 'ON' : 'OFF' }
-                                </span>
-                            </IconButton>
-                            <IconButton onClick={() => setReplies(replies.filter((_, i) => i !== index))}>
-                                <XCircle size={16} />
-                            </IconButton>
-                        </span>
-                    </Base>
-                )
-            }) }
-        </div>
-    )
+	useEffect(() => {
+		return internalSubscribe(
+			"ReplyBar",
+			"add",
+			(id) =>
+				replies.length < MAX_REPLIES &&
+				!replies.find((x) => x.id === id) &&
+				setReplies([...replies, { id, mention: false }]),
+		);
+	}, [replies]);
+
+	const view = useRenderState(channel);
+	if (view?.type !== "RENDER") return null;
+
+	const ids = replies.map((x) => x.id);
+	const messages = view.messages.filter((x) => ids.includes(x._id));
+	const users = useUsers(messages.map((x) => x.author));
+
+	return (
+		<div>
+			{replies.map((reply, index) => {
+				let message = messages.find((x) => reply.id === x._id);
+				// ! FIXME: better solution would be to
+				// ! have a hook for resolving messages from
+				// ! render state along with relevant users
+				// -> which then fetches any unknown messages
+				if (!message)
+					return (
+						<span>
+							<Text id="app.main.channel.misc.failed_load" />
+						</span>
+					);
+
+				let user = users.find((x) => message!.author === x?._id);
+				if (!user) return;
+
+				return (
+					<Base key={reply.id}>
+						<ReplyBase preview>
+							<ReplyIcon size={22} />
+							<UserShort user={user} size={16} />
+							{message.attachments &&
+								message.attachments.length > 0 && (
+									<File size={16} />
+								)}
+							<Markdown
+								disallowBigEmoji
+								content={(message.content as string).replace(
+									/\n/g,
+									" ",
+								)}
+							/>
+						</ReplyBase>
+						<span class="actions">
+							<IconButton
+								onClick={() =>
+									setReplies(
+										replies.map((_, i) =>
+											i === index
+												? { ..._, mention: !_.mention }
+												: _,
+										),
+									)
+								}>
+								<span class="toggle">
+									<At size={16} />{" "}
+									{reply.mention ? "ON" : "OFF"}
+								</span>
+							</IconButton>
+							<IconButton
+								onClick={() =>
+									setReplies(
+										replies.filter((_, i) => i !== index),
+									)
+								}>
+								<XCircle size={16} />
+							</IconButton>
+						</span>
+					</Base>
+				);
+			})}
+		</div>
+	);
 }
diff --git a/src/components/common/messaging/bars/TypingIndicator.tsx b/src/components/common/messaging/bars/TypingIndicator.tsx
index d88601590166ef2a4d7eafea917ce16802f77b89..a00e2ff9854a7d62b7bbb37a73993d0d25a3096c 100644
--- a/src/components/common/messaging/bars/TypingIndicator.tsx
+++ b/src/components/common/messaging/bars/TypingIndicator.tsx
@@ -1,111 +1,121 @@
-import { User } from 'revolt.js';
+import { User } from "revolt.js";
+import styled from "styled-components";
+
 import { Text } from "preact-i18n";
-import styled from 'styled-components';
-import { useContext } from 'preact/hooks';
-import { connectState } from '../../../../redux/connector';
-import { useUsers } from '../../../../context/revoltjs/hooks';
-import { TypingUser } from '../../../../redux/reducers/typing';
-import { AppContext } from '../../../../context/revoltjs/RevoltClient';
+import { useContext } from "preact/hooks";
+
+import { connectState } from "../../../../redux/connector";
+import { TypingUser } from "../../../../redux/reducers/typing";
+
+import { AppContext } from "../../../../context/revoltjs/RevoltClient";
+import { useUsers } from "../../../../context/revoltjs/hooks";
 
 interface Props {
-    typing?: TypingUser[]
+	typing?: TypingUser[];
 }
 
 const Base = styled.div`
-    position: relative;
-
-    > div {
-        height: 24px;
-        margin-top: -24px;
-        position: absolute;
-
-        gap: 8px;
-        display: flex;
-        padding: 0 10px;
-        user-select: none;
-        align-items: center;
-        flex-direction: row;
-        width: calc(100% - 3px);
-        color: var(--secondary-foreground);
-        background: var(--secondary-background);
-    }
-
-    .avatars {
-        display: flex;
-        
-        img {
-            width: 16px;
-            height: 16px;
-            object-fit: cover;
-            border-radius: 50%;
-
-            &:not(:first-child) {
-                margin-left: -4px;
-            }
-        }
-    }
-
-    .usernames {
-        min-width: 0;
-        font-size: 13px;
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-    }
+	position: relative;
+
+	> div {
+		height: 24px;
+		margin-top: -24px;
+		position: absolute;
+
+		gap: 8px;
+		display: flex;
+		padding: 0 10px;
+		user-select: none;
+		align-items: center;
+		flex-direction: row;
+		width: calc(100% - 3px);
+		color: var(--secondary-foreground);
+		background: var(--secondary-background);
+	}
+
+	.avatars {
+		display: flex;
+
+		img {
+			width: 16px;
+			height: 16px;
+			object-fit: cover;
+			border-radius: 50%;
+
+			&:not(:first-child) {
+				margin-left: -4px;
+			}
+		}
+	}
+
+	.usernames {
+		min-width: 0;
+		font-size: 13px;
+		overflow: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
 `;
 
 export function TypingIndicator({ typing }: Props) {
-    if (typing && typing.length > 0) {
-        const client = useContext(AppContext);
-        const users = useUsers(typing.map(x => x.id))
-            .filter(x => typeof x !== 'undefined') as User[];
-
-        users.sort((a, b) => a._id.toUpperCase().localeCompare(b._id.toUpperCase()));
-
-        let text;
-        if (users.length >= 5) {
-            text = <Text id="app.main.channel.typing.several" />;
-        } else if (users.length > 1) {
-            const usersCopy = [...users];
-            text = (
-                <Text
-                    id="app.main.channel.typing.multiple"
-                    fields={{
-                        user: usersCopy.pop()?.username,
-                        userlist: usersCopy.map(x => x.username).join(", ")
-                    }}
-                />
-            );
-        } else {
-            text = (
-                <Text
-                    id="app.main.channel.typing.single"
-                    fields={{ user: users[0].username }}
-                />
-            );
-        }
-
-        return (
-            <Base>
-                <div>
-                    <div className="avatars">
-                        {users.map(user => (
-                            <img
-                                src={client.users.getAvatarURL(user._id, { max_side: 256 }, true)}
-                            />
-                        ))}
-                    </div>
-                    <div className="usernames">{text}</div>
-                </div>
-            </Base>
-        );
-    }
-
-    return null;
+	if (typing && typing.length > 0) {
+		const client = useContext(AppContext);
+		const users = useUsers(typing.map((x) => x.id)).filter(
+			(x) => typeof x !== "undefined",
+		) as User[];
+
+		users.sort((a, b) =>
+			a._id.toUpperCase().localeCompare(b._id.toUpperCase()),
+		);
+
+		let text;
+		if (users.length >= 5) {
+			text = <Text id="app.main.channel.typing.several" />;
+		} else if (users.length > 1) {
+			const usersCopy = [...users];
+			text = (
+				<Text
+					id="app.main.channel.typing.multiple"
+					fields={{
+						user: usersCopy.pop()?.username,
+						userlist: usersCopy.map((x) => x.username).join(", "),
+					}}
+				/>
+			);
+		} else {
+			text = (
+				<Text
+					id="app.main.channel.typing.single"
+					fields={{ user: users[0].username }}
+				/>
+			);
+		}
+
+		return (
+			<Base>
+				<div>
+					<div className="avatars">
+						{users.map((user) => (
+							<img
+								src={client.users.getAvatarURL(
+									user._id,
+									{ max_side: 256 },
+									true,
+								)}
+							/>
+						))}
+					</div>
+					<div className="usernames">{text}</div>
+				</div>
+			</Base>
+		);
+	}
+
+	return null;
 }
 
 export default connectState<{ id: string }>(TypingIndicator, (state, props) => {
-    return {
-        typing: state.typing && state.typing[props.id]
-    };
+	return {
+		typing: state.typing && state.typing[props.id],
+	};
 });
diff --git a/src/components/common/messaging/embed/Embed.tsx b/src/components/common/messaging/embed/Embed.tsx
index f0bd8d78d401ee527f4913fb39f57eee1f52191e..fa7a09fb8d772b6ba0f36f9e5732673d629a739a 100644
--- a/src/components/common/messaging/embed/Embed.tsx
+++ b/src/components/common/messaging/embed/Embed.tsx
@@ -1,13 +1,16 @@
-import classNames from 'classnames';
-import EmbedMedia from './EmbedMedia';
-import styles from "./Embed.module.scss";
-import { useContext } from 'preact/hooks';
 import { Embed as EmbedRJS } from "revolt.js/dist/api/objects";
-import { useIntermediate } from '../../../../context/intermediate/Intermediate';
-import { MessageAreaWidthContext } from '../../../../pages/channels/messaging/MessageArea';
+
+import styles from "./Embed.module.scss";
+import classNames from "classnames";
+import { useContext } from "preact/hooks";
+
+import { useIntermediate } from "../../../../context/intermediate/Intermediate";
+
+import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
+import EmbedMedia from "./EmbedMedia";
 
 interface Props {
-    embed: EmbedRJS;
+	embed: EmbedRJS;
 }
 
 const MAX_EMBED_WIDTH = 480;
@@ -16,113 +19,149 @@ const CONTAINER_PADDING = 24;
 const MAX_PREVIEW_SIZE = 150;
 
 export default function Embed({ embed }: Props) {
-    // ! FIXME: temp code
-    // ! add proxy function to client
-    function proxyImage(url: string) {
-        return 'https://jan.revolt.chat/proxy?url=' + encodeURIComponent(url);
-    }
-
-    const { openScreen } = useIntermediate();
-    const maxWidth = Math.min(useContext(MessageAreaWidthContext) - CONTAINER_PADDING, MAX_EMBED_WIDTH);
-
-    function calculateSize(w: number, h: number): { width: number, height: number } {
-        let limitingWidth = Math.min(
-            maxWidth,
-            w
-        );
-
-        let limitingHeight = Math.min(
-            MAX_EMBED_HEIGHT,
-            h
-        );
-
-        // Calculate smallest possible WxH.
-        let width = Math.min(
-            limitingWidth,
-            limitingHeight * (w / h)
-        );
-
-        let height = Math.min(
-            limitingHeight,
-            limitingWidth * (h / w)
-        );
-
-        return { width, height };
-    }
-
-    switch (embed.type) {
-        case 'Website': {
-            // Determine special embed size.
-            let mw, mh;
-            let largeMedia = (embed.special && embed.special.type !== 'None') || embed.image?.size === 'Large';
-            switch (embed.special?.type) {
-                case 'YouTube':
-                case 'Bandcamp': {
-                    mw = embed.video?.width ?? 1280;
-                    mh = embed.video?.height ?? 720;
-                    break;
-                }
-                case 'Twitch': {
-                    mw = 1280;
-                    mh = 720;
-                    break;
-                }
-                default: {
-                    if (embed.image?.size === 'Preview') {
-                        mw = MAX_EMBED_WIDTH;
-                        mh = Math.min(embed.image.height ?? 0, MAX_PREVIEW_SIZE);
-                    } else {
-                        mw = embed.image?.width ?? MAX_EMBED_WIDTH;
-                        mh = embed.image?.height ?? 0;
-                    }
-                }
-            }
-
-            let { width, height } = calculateSize(mw, mh);
-            return (
-                <div
-                    className={classNames(styles.embed, styles.website)}
-                    style={{
-                        borderInlineStartColor: embed.color ?? 'var(--tertiary-background)',
-                        width: width + CONTAINER_PADDING
-                    }}>
-                    <div>
-                        { embed.site_name && <div className={styles.siteinfo}>
-                            { embed.icon_url && <img className={styles.favicon} src={proxyImage(embed.icon_url)} draggable={false} onError={e => e.currentTarget.style.display = 'none'} /> }
-                            <div className={styles.site}>{ embed.site_name } </div>
-                        </div> }
-
-                        {/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
-                        { embed.title && <span><a href={embed.url} target={"_blank"} className={styles.title}>{ embed.title }</a></span> }
-                        { embed.description && <div className={styles.description}>{ embed.description }</div> }
-
-                        { largeMedia && <EmbedMedia embed={embed} height={height} /> }
-                    </div>
-                    {
-                        !largeMedia && <div>
-                            <EmbedMedia embed={embed} width={height * ((embed.image?.width ?? 0) / (embed.image?.height ?? 0))} height={height} />
-                        </div>
-                    }
-                </div>
-            )
-        }
-        case 'Image': {
-            return (
-                <img className={classNames(styles.embed, styles.image)}
-                    style={calculateSize(embed.width, embed.height)}
-                    src={proxyImage(embed.url)}
-                    type="text/html"
-                    frameBorder="0"
-                    onClick={() =>
-                        openScreen({ id: "image_viewer", embed })
-                    }
-                    onMouseDown={ev =>
-                        ev.button === 1 &&
-                        window.open(embed.url, "_blank")
-                    }
-                />
-            )
-        }
-        default: return null;
-    }
+	// ! FIXME: temp code
+	// ! add proxy function to client
+	function proxyImage(url: string) {
+		return "https://jan.revolt.chat/proxy?url=" + encodeURIComponent(url);
+	}
+
+	const { openScreen } = useIntermediate();
+	const maxWidth = Math.min(
+		useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
+		MAX_EMBED_WIDTH,
+	);
+
+	function calculateSize(
+		w: number,
+		h: number,
+	): { width: number; height: number } {
+		let limitingWidth = Math.min(maxWidth, w);
+
+		let limitingHeight = Math.min(MAX_EMBED_HEIGHT, h);
+
+		// Calculate smallest possible WxH.
+		let width = Math.min(limitingWidth, limitingHeight * (w / h));
+
+		let height = Math.min(limitingHeight, limitingWidth * (h / w));
+
+		return { width, height };
+	}
+
+	switch (embed.type) {
+		case "Website": {
+			// Determine special embed size.
+			let mw, mh;
+			let largeMedia =
+				(embed.special && embed.special.type !== "None") ||
+				embed.image?.size === "Large";
+			switch (embed.special?.type) {
+				case "YouTube":
+				case "Bandcamp": {
+					mw = embed.video?.width ?? 1280;
+					mh = embed.video?.height ?? 720;
+					break;
+				}
+				case "Twitch": {
+					mw = 1280;
+					mh = 720;
+					break;
+				}
+				default: {
+					if (embed.image?.size === "Preview") {
+						mw = MAX_EMBED_WIDTH;
+						mh = Math.min(
+							embed.image.height ?? 0,
+							MAX_PREVIEW_SIZE,
+						);
+					} else {
+						mw = embed.image?.width ?? MAX_EMBED_WIDTH;
+						mh = embed.image?.height ?? 0;
+					}
+				}
+			}
+
+			let { width, height } = calculateSize(mw, mh);
+			return (
+				<div
+					className={classNames(styles.embed, styles.website)}
+					style={{
+						borderInlineStartColor:
+							embed.color ?? "var(--tertiary-background)",
+						width: width + CONTAINER_PADDING,
+					}}>
+					<div>
+						{embed.site_name && (
+							<div className={styles.siteinfo}>
+								{embed.icon_url && (
+									<img
+										className={styles.favicon}
+										src={proxyImage(embed.icon_url)}
+										draggable={false}
+										onError={(e) =>
+											(e.currentTarget.style.display =
+												"none")
+										}
+									/>
+								)}
+								<div className={styles.site}>
+									{embed.site_name}{" "}
+								</div>
+							</div>
+						)}
+
+						{/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
+						{embed.title && (
+							<span>
+								<a
+									href={embed.url}
+									target={"_blank"}
+									className={styles.title}>
+									{embed.title}
+								</a>
+							</span>
+						)}
+						{embed.description && (
+							<div className={styles.description}>
+								{embed.description}
+							</div>
+						)}
+
+						{largeMedia && (
+							<EmbedMedia embed={embed} height={height} />
+						)}
+					</div>
+					{!largeMedia && (
+						<div>
+							<EmbedMedia
+								embed={embed}
+								width={
+									height *
+									((embed.image?.width ?? 0) /
+										(embed.image?.height ?? 0))
+								}
+								height={height}
+							/>
+						</div>
+					)}
+				</div>
+			);
+		}
+		case "Image": {
+			return (
+				<img
+					className={classNames(styles.embed, styles.image)}
+					style={calculateSize(embed.width, embed.height)}
+					src={proxyImage(embed.url)}
+					type="text/html"
+					frameBorder="0"
+					onClick={() => openScreen({ id: "image_viewer", embed })}
+					onMouseDown={(ev) =>
+						ev.button === 1 && window.open(embed.url, "_blank")
+					}
+				/>
+			);
+		}
+		default:
+			return null;
+	}
 }
diff --git a/src/components/common/messaging/embed/EmbedMedia.tsx b/src/components/common/messaging/embed/EmbedMedia.tsx
index 70c045798e901719e3f045d8188296e40825bead..b97922d35d02eba7df29d21f7f692e138be6dbc5 100644
--- a/src/components/common/messaging/embed/EmbedMedia.tsx
+++ b/src/components/common/messaging/embed/EmbedMedia.tsx
@@ -1,78 +1,100 @@
-import styles from './Embed.module.scss';
 import { Embed } from "revolt.js/dist/api/objects";
-import { useIntermediate } from '../../../../context/intermediate/Intermediate';
+
+import styles from "./Embed.module.scss";
+
+import { useIntermediate } from "../../../../context/intermediate/Intermediate";
 
 interface Props {
-    embed: Embed;
-    width?: number;
-    height: number;
+	embed: Embed;
+	width?: number;
+	height: number;
 }
 
 export default function EmbedMedia({ embed, width, height }: Props) {
-    // ! FIXME: temp code
-    // ! add proxy function to client
-    function proxyImage(url: string) {
-        return 'https://jan.revolt.chat/proxy?url=' + encodeURIComponent(url);
-    }
+	// ! FIXME: temp code
+	// ! add proxy function to client
+	function proxyImage(url: string) {
+		return "https://jan.revolt.chat/proxy?url=" + encodeURIComponent(url);
+	}
 
-    if (embed.type !== 'Website') return null;
-    const { openScreen } = useIntermediate();
+	if (embed.type !== "Website") return null;
+	const { openScreen } = useIntermediate();
 
-    switch (embed.special?.type) {
-        case 'YouTube': return (
-            <iframe 
-                src={`https://www.youtube-nocookie.com/embed/${embed.special.id}?modestbranding=1`}
-                allowFullScreen
-                style={{ height }} />
-        )
-        case 'Twitch': return (
-            <iframe
-                src={`https://player.twitch.tv/?${embed.special.content_type.toLowerCase()}=${embed.special.id}&parent=${window.location.hostname}&autoplay=false`}
-                frameBorder="0"
-                allowFullScreen
-                scrolling="no"
-                style={{ height, }} />
-        )
-        case 'Spotify': return (
-            <iframe
-                src={`https://open.spotify.com/embed/${embed.special.content_type}/${embed.special.id}`}
-                frameBorder="0"
-                allowFullScreen
-                allowTransparency
-                style={{ height }} />
-        )
-        case 'Soundcloud': return (
-            <iframe
-                src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(embed.url!)}&color=%23FF7F50&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true`}
-                frameBorder="0"
-                scrolling="no"
-                style={{ height }} />
-        )
-        case 'Bandcamp': {
-            return <iframe
-                src={`https://bandcamp.com/EmbeddedPlayer/${embed.special.content_type.toLowerCase()}=${embed.special.id}/size=large/bgcol=181a1b/linkcol=056cc4/tracklist=false/transparent=true/`}
-                seamless
-                style={{ height }} />;
-        }
-        default: {
-            if (embed.image) {
-                let url = embed.image.url;
-                return (
-                    <img
-                        className={styles.image}
-                        src={proxyImage(url)}
-                        style={{ width, height }}
-                        onClick={() =>
-                            openScreen({ id: "image_viewer", embed: embed.image })
-                        }
-                        onMouseDown={ev =>
-                            ev.button === 1 &&
-                            window.open(url, "_blank")
-                        } />
-                );
-            }
-        }
-    }
+	switch (embed.special?.type) {
+		case "YouTube":
+			return (
+				<iframe
+					src={`https://www.youtube-nocookie.com/embed/${embed.special.id}?modestbranding=1`}
+					allowFullScreen
+					style={{ height }}
+				/>
+			);
+		case "Twitch":
+			return (
+				<iframe
+					src={`https://player.twitch.tv/?${embed.special.content_type.toLowerCase()}=${
+						embed.special.id
+					}&parent=${window.location.hostname}&autoplay=false`}
+					frameBorder="0"
+					allowFullScreen
+					scrolling="no"
+					style={{ height }}
+				/>
+			);
+		case "Spotify":
+			return (
+				<iframe
+					src={`https://open.spotify.com/embed/${embed.special.content_type}/${embed.special.id}`}
+					frameBorder="0"
+					allowFullScreen
+					allowTransparency
+					style={{ height }}
+				/>
+			);
+		case "Soundcloud":
+			return (
+				<iframe
+					src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(
+						embed.url!,
+					)}&color=%23FF7F50&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true`}
+					frameBorder="0"
+					scrolling="no"
+					style={{ height }}
+				/>
+			);
+		case "Bandcamp": {
+			return (
+				<iframe
+					src={`https://bandcamp.com/EmbeddedPlayer/${embed.special.content_type.toLowerCase()}=${
+						embed.special.id
+					}/size=large/bgcol=181a1b/linkcol=056cc4/tracklist=false/transparent=true/`}
+					seamless
+					style={{ height }}
+				/>
+			);
+		}
+		default: {
+			if (embed.image) {
+				let url = embed.image.url;
+				return (
+					<img
+						className={styles.image}
+						src={proxyImage(url)}
+						style={{ width, height }}
+						onClick={() =>
+							openScreen({
+								id: "image_viewer",
+								embed: embed.image,
+							})
+						}
+						onMouseDown={(ev) =>
+							ev.button === 1 && window.open(url, "_blank")
+						}
+					/>
+				);
+			}
+		}
+	}
 
-    return null;
+	return null;
 }
diff --git a/src/components/common/messaging/embed/EmbedMediaActions.tsx b/src/components/common/messaging/embed/EmbedMediaActions.tsx
index ab3d714428dbcf8016a17298b9989adfc0079da9..1f7dd4c79eaf3809e8a76ba83d7deda7efa3c43e 100644
--- a/src/components/common/messaging/embed/EmbedMediaActions.tsx
+++ b/src/components/common/messaging/embed/EmbedMediaActions.tsx
@@ -1,26 +1,30 @@
-import styles from './Embed.module.scss';
-import IconButton from '../../../ui/IconButton';
-import { LinkExternal } from '@styled-icons/boxicons-regular';
+import { LinkExternal } from "@styled-icons/boxicons-regular";
 import { EmbedImage } from "revolt.js/dist/api/objects";
 
+import styles from "./Embed.module.scss";
+
+import IconButton from "../../../ui/IconButton";
+
 interface Props {
-    embed: EmbedImage;
+	embed: EmbedImage;
 }
 
 export default function EmbedMediaActions({ embed }: Props) {
-    const filename = embed.url.split('/').pop();
+	const filename = embed.url.split("/").pop();
 
-    return (
-        <div className={styles.actions}>
-            <div className={styles.info}>
-                <span className={styles.filename}>{filename}</span>
-                <span className={styles.filesize}>{embed.width + 'x' + embed.height}</span>
-            </div>
-            <a href={embed.url} target="_blank">
-                <IconButton>
-                    <LinkExternal size={24} />
-                </IconButton>
-            </a>
-        </div>
-    )
+	return (
+		<div className={styles.actions}>
+			<div className={styles.info}>
+				<span className={styles.filename}>{filename}</span>
+				<span className={styles.filesize}>
+					{embed.width + "x" + embed.height}
+				</span>
+			</div>
+			<a href={embed.url} target="_blank">
+				<IconButton>
+					<LinkExternal size={24} />
+				</IconButton>
+			</a>
+		</div>
+	);
 }
diff --git a/src/components/common/user/UserCheckbox.tsx b/src/components/common/user/UserCheckbox.tsx
index 05cb69f71eab4b8cdcbafef8fd2ec952e48b3a77..7e204dd9919b1ad958a2520c573235e08d71073c 100644
--- a/src/components/common/user/UserCheckbox.tsx
+++ b/src/components/common/user/UserCheckbox.tsx
@@ -1,14 +1,16 @@
 import { User } from "revolt.js";
-import UserIcon from "./UserIcon";
+
 import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
 
+import UserIcon from "./UserIcon";
+
 type UserProps = Omit<CheckboxProps, "children"> & { user: User };
 
 export default function UserCheckbox({ user, ...props }: UserProps) {
-    return (
-        <Checkbox {...props}>
-            <UserIcon target={user} size={32} />
-            {user.username}
-        </Checkbox>
-    );
+	return (
+		<Checkbox {...props}>
+			<UserIcon target={user} size={32} />
+			{user.username}
+		</Checkbox>
+	);
 }
diff --git a/src/components/common/user/UserHeader.tsx b/src/components/common/user/UserHeader.tsx
index d234340e42d1158bb4df60e126846a117827b58e..9783b3a3c02a6118a65fee47e5da6609f1671800 100644
--- a/src/components/common/user/UserHeader.tsx
+++ b/src/components/common/user/UserHeader.tsx
@@ -1,75 +1,84 @@
-import Tooltip from "../Tooltip";
+import { Cog } from "@styled-icons/boxicons-solid";
+import { Link } from "react-router-dom";
 import { User } from "revolt.js";
-import UserIcon from "./UserIcon";
-import { Text } from "preact-i18n";
-import Header from "../../ui/Header";
-import UserStatus from './UserStatus';
 import styled from "styled-components";
-import { Localizer } from 'preact-i18n';
-import { Link } from "react-router-dom";
-import IconButton from "../../ui/IconButton";
-import { Cog } from "@styled-icons/boxicons-solid";
+
 import { openContextMenu } from "preact-context-menu";
+import { Text } from "preact-i18n";
+import { Localizer } from "preact-i18n";
+
 import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
 
+import Header from "../../ui/Header";
+import IconButton from "../../ui/IconButton";
+
+import Tooltip from "../Tooltip";
+import UserIcon from "./UserIcon";
+import UserStatus from "./UserStatus";
+
 const HeaderBase = styled.div`
-    gap: 0;
-    flex-grow: 1;
-    min-width: 0;
-    display: flex;
-    flex-direction: column;
-    
-    * {
-        min-width: 0;
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-    }
+	gap: 0;
+	flex-grow: 1;
+	min-width: 0;
+	display: flex;
+	flex-direction: column;
+
+	* {
+		min-width: 0;
+		overflow: hidden;
+		white-space: nowrap;
+		text-overflow: ellipsis;
+	}
 
-    .username {
-        cursor: pointer;
-        font-size: 16px;
-        font-weight: 600;
-    }
+	.username {
+		cursor: pointer;
+		font-size: 16px;
+		font-weight: 600;
+	}
 
-    .status {
-        cursor: pointer;
-        font-size: 12px;
-        margin-top: -2px;
-    }
+	.status {
+		cursor: pointer;
+		font-size: 12px;
+		margin-top: -2px;
+	}
 `;
 
 interface Props {
-    user: User
+	user: User;
 }
 
 export default function UserHeader({ user }: Props) {
-    const { writeClipboard } = useIntermediate();
+	const { writeClipboard } = useIntermediate();
 
-    return (
-        <Header borders placement="secondary">
-            <HeaderBase>
-                <Localizer>
-                    <Tooltip content={<Text id="app.special.copy_username" />}>
-                        <span className="username"
-                            onClick={() => writeClipboard(user.username)}>
-                            @{user.username}
-                        </span>
-                    </Tooltip>
-                </Localizer>
-                <span className="status"
-                    onClick={() => openContextMenu("Status")}>
-                    <UserStatus user={user} />
-                </span>
-            </HeaderBase>
-            { !isTouchscreenDevice && <div className="actions">
-                <Link to="/settings">
-                    <IconButton>
-                        <Cog size={24} />
-                    </IconButton>
-                </Link>
-            </div> }
-        </Header>
-    )
+	return (
+		<Header borders placement="secondary">
+			<HeaderBase>
+				<Localizer>
+					<Tooltip content={<Text id="app.special.copy_username" />}>
+						<span
+							className="username"
+							onClick={() => writeClipboard(user.username)}>
+							@{user.username}
+						</span>
+					</Tooltip>
+				</Localizer>
+				<span
+					className="status"
+					onClick={() => openContextMenu("Status")}>
+					<UserStatus user={user} />
+				</span>
+			</HeaderBase>
+			{!isTouchscreenDevice && (
+				<div className="actions">
+					<Link to="/settings">
+						<IconButton>
+							<Cog size={24} />
+						</IconButton>
+					</Link>
+				</div>
+			)}
+		</Header>
+	);
 }
diff --git a/src/components/common/user/UserIcon.tsx b/src/components/common/user/UserIcon.tsx
index 724f042ee7eb13688cfd98d470b67e51b2b4e2cf..d1c2fb9c3f95a5625e1e1ce1a2ac7a80f3b1387f 100644
--- a/src/components/common/user/UserIcon.tsx
+++ b/src/components/common/user/UserIcon.tsx
@@ -1,93 +1,104 @@
-import { User } from "revolt.js";
-import { useContext } from "preact/hooks";
 import { MicrophoneOff } from "@styled-icons/boxicons-regular";
-import styled, { css } from "styled-components";
+import { User } from "revolt.js";
 import { Users } from "revolt.js/dist/api/objects";
+import styled, { css } from "styled-components";
+
+import { useContext } from "preact/hooks";
+
 import { ThemeContext } from "../../../context/Theme";
-import IconBase, { IconBaseProps } from "../IconBase";
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 
+import IconBase, { IconBaseProps } from "../IconBase";
+import fallback from "../assets/user.png";
+
 type VoiceStatus = "muted";
 interface Props extends IconBaseProps<User> {
-    mask?: string;
-    status?: boolean;
-    voice?: VoiceStatus;
+	mask?: string;
+	status?: boolean;
+	voice?: VoiceStatus;
 }
 
 export function useStatusColour(user?: User) {
-    const theme = useContext(ThemeContext);
+	const theme = useContext(ThemeContext);
 
-    return (
-        user?.online &&
-            user?.status?.presence !== Users.Presence.Invisible
-                ? user?.status?.presence === Users.Presence.Idle
-                    ? theme["status-away"]
-                    : user?.status?.presence ===
-                        Users.Presence.Busy
-                    ? theme["status-busy"]
-                    : theme["status-online"]
-                : theme["status-invisible"]
-    );
+	return user?.online && user?.status?.presence !== Users.Presence.Invisible
+		? user?.status?.presence === Users.Presence.Idle
+			? theme["status-away"]
+			: user?.status?.presence === Users.Presence.Busy
+			? theme["status-busy"]
+			: theme["status-online"]
+		: theme["status-invisible"];
 }
 
 const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
+	width: 10px;
+	height: 10px;
+	border-radius: 50%;
 
-    display: flex;
-    align-items: center;
-    justify-content: center;
+	display: flex;
+	align-items: center;
+	justify-content: center;
 
-    svg {
-        stroke: white;
-    }
+	svg {
+		stroke: white;
+	}
 
-    ${ props => props.status === 'muted' && css`
-        background: var(--error);
-    ` }
+	${(props) =>
+		props.status === "muted" &&
+		css`
+			background: var(--error);
+		`}
 `;
 
-import fallback from '../assets/user.png';
-
-export default function UserIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) {
-    const client = useContext(AppContext);
+export default function UserIcon(
+	props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>,
+) {
+	const client = useContext(AppContext);
 
-    const { target, attachment, size, voice, status, animate, mask, children, as, ...svgProps } = props;
-    const iconURL = client.generateFileURL(target?.avatar ?? attachment, { max_side: 256 }, animate)
-        ?? (target ? client.users.getDefaultAvatarURL(target._id) : fallback);
+	const {
+		target,
+		attachment,
+		size,
+		voice,
+		status,
+		animate,
+		mask,
+		children,
+		as,
+		...svgProps
+	} = props;
+	const iconURL =
+		client.generateFileURL(
+			target?.avatar ?? attachment,
+			{ max_side: 256 },
+			animate,
+		) ?? (target ? client.users.getDefaultAvatarURL(target._id) : fallback);
 
-    return (
-        <IconBase {...svgProps}
-            width={size}
-            height={size}
-            aria-hidden="true"
-            viewBox="0 0 32 32">
-            <foreignObject x="0" y="0" width="32" height="32" mask={mask ?? (status ? "url(#user)" : undefined)}>
-                {
-                    <img src={iconURL}
-                        draggable={false} />
-                }
-            </foreignObject>
-            {props.status && (
-                <circle
-                    cx="27"
-                    cy="27"
-                    r="5"
-                    fill={useStatusColour(target)}
-                />
-            )}
-            {props.voice && (
-                <foreignObject
-                    x="22"
-                    y="22"
-                    width="10"
-                    height="10">
-                    <VoiceIndicator status={props.voice}>
-                        {props.voice === "muted" && <MicrophoneOff size={6} />}
-                    </VoiceIndicator>
-                </foreignObject>
-            )}
-        </IconBase>
-    );
+	return (
+		<IconBase
+			{...svgProps}
+			width={size}
+			height={size}
+			aria-hidden="true"
+			viewBox="0 0 32 32">
+			<foreignObject
+				x="0"
+				y="0"
+				width="32"
+				height="32"
+				mask={mask ?? (status ? "url(#user)" : undefined)}>
+				{<img src={iconURL} draggable={false} />}
+			</foreignObject>
+			{props.status && (
+				<circle cx="27" cy="27" r="5" fill={useStatusColour(target)} />
+			)}
+			{props.voice && (
+				<foreignObject x="22" y="22" width="10" height="10">
+					<VoiceIndicator status={props.voice}>
+						{props.voice === "muted" && <MicrophoneOff size={6} />}
+					</VoiceIndicator>
+				</foreignObject>
+			)}
+		</IconBase>
+	);
 }
diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx
index f0cec4eda4b0337db9fc4fa3825ccac770b3a054..3c2b4aa00e465ce6b8ae31b33edaed8a63463985 100644
--- a/src/components/common/user/UserShort.tsx
+++ b/src/components/common/user/UserShort.tsx
@@ -1,14 +1,31 @@
 import { User } from "revolt.js";
-import UserIcon from "./UserIcon";
+
 import { Text } from "preact-i18n";
 
-export function Username({ user, ...otherProps }: { user?: User } & JSX.HTMLAttributes<HTMLElement>) {
-    return <span {...otherProps}>{ user?.username ?? <Text id="app.main.channel.unknown_user" /> }</span>;
+import UserIcon from "./UserIcon";
+
+export function Username({
+	user,
+	...otherProps
+}: { user?: User } & JSX.HTMLAttributes<HTMLElement>) {
+	return (
+		<span {...otherProps}>
+			{user?.username ?? <Text id="app.main.channel.unknown_user" />}
+		</span>
+	);
 }
 
-export default function UserShort({ user, size }: { user?: User, size?: number }) {
-    return <>
-        <UserIcon size={size ?? 24} target={user} />
-        <Username user={user} />
-    </>;
-}
\ No newline at end of file
+export default function UserShort({
+	user,
+	size,
+}: {
+	user?: User;
+	size?: number;
+}) {
+	return (
+		<>
+			<UserIcon size={size ?? 24} target={user} />
+			<Username user={user} />
+		</>
+	);
+}
diff --git a/src/components/common/user/UserStatus.tsx b/src/components/common/user/UserStatus.tsx
index 5a525517ecbc4878d132c5c93e02143537639abe..d4b3f60cc3ef20a728b0e904a12c10d5bb45c116 100644
--- a/src/components/common/user/UserStatus.tsx
+++ b/src/components/common/user/UserStatus.tsx
@@ -1,31 +1,32 @@
 import { User } from "revolt.js";
-import { Text } from "preact-i18n";
 import { Users } from "revolt.js/dist/api/objects";
 
+import { Text } from "preact-i18n";
+
 interface Props {
-    user: User;
+	user: User;
 }
 
 export default function UserStatus({ user }: Props) {
-    if (user.online) {
-        if (user.status?.text) {
-            return <>{user.status?.text}</>;
-        }
+	if (user.online) {
+		if (user.status?.text) {
+			return <>{user.status?.text}</>;
+		}
 
-        if (user.status?.presence === Users.Presence.Busy) {
-            return <Text id="app.status.busy" />;
-        }
+		if (user.status?.presence === Users.Presence.Busy) {
+			return <Text id="app.status.busy" />;
+		}
 
-        if (user.status?.presence === Users.Presence.Idle) {
-            return <Text id="app.status.idle" />;
-        }
+		if (user.status?.presence === Users.Presence.Idle) {
+			return <Text id="app.status.idle" />;
+		}
 
-        if (user.status?.presence === Users.Presence.Invisible) {
-            return <Text id="app.status.offline" />;
-        }
+		if (user.status?.presence === Users.Presence.Invisible) {
+			return <Text id="app.status.offline" />;
+		}
 
-        return <Text id="app.status.online" />;
-    }
+		return <Text id="app.status.online" />;
+	}
 
-    return <Text id="app.status.offline" />;
+	return <Text id="app.status.offline" />;
 }
diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx
index 1dffb1c07b61cb3ecee05003482494b3e3a04054..fbd274d087310380240300e2a371b49a9da32cb8 100644
--- a/src/components/markdown/Markdown.tsx
+++ b/src/components/markdown/Markdown.tsx
@@ -1,17 +1,17 @@
 import { Suspense, lazy } from "preact/compat";
 
-const Renderer = lazy(() => import('./Renderer'));
+const Renderer = lazy(() => import("./Renderer"));
 
 export interface MarkdownProps {
-    content?: string;
-    disallowBigEmoji?: boolean;
+	content?: string;
+	disallowBigEmoji?: boolean;
 }
 
 export default function Markdown(props: MarkdownProps) {
-    return (
-        // @ts-expect-error
-        <Suspense fallback={props.content}>
-            <Renderer {...props} />
-        </Suspense>
-    )
+	return (
+		// @ts-expect-error
+		<Suspense fallback={props.content}>
+			<Renderer {...props} />
+		</Suspense>
+	);
 }
diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx
index 61575a8650f59c71ce6897bd9e8a9ac2d7a34f83..a1dafb90a1268db572bb74023ecc35fc28ff1c35 100644
--- a/src/components/markdown/Renderer.tsx
+++ b/src/components/markdown/Renderer.tsx
@@ -1,187 +1,192 @@
+import MarkdownKatex from "@traptitech/markdown-it-katex";
+import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
+import "katex/dist/katex.min.css";
 import MarkdownIt from "markdown-it";
+// @ts-ignore
+import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
+// @ts-ignore
+import MarkdownSub from "markdown-it-sub";
+// @ts-ignore
+import MarkdownSup from "markdown-it-sup";
+import Prism from "prismjs";
+import "prismjs/themes/prism-tomorrow.css";
 import { RE_MENTIONS } from "revolt.js";
-import { useContext } from "preact/hooks";
-import { MarkdownProps } from "./Markdown";
+
 import styles from "./Markdown.module.scss";
-import { generateEmoji } from "../common/Emoji";
+import { useContext } from "preact/hooks";
+
 import { internalEmit } from "../../lib/eventEmitter";
-import { emojiDictionary } from "../../assets/emojis";
-import { AppContext } from "../../context/revoltjs/RevoltClient";
 
-import Prism from "prismjs";
-import "katex/dist/katex.min.css";
-import "prismjs/themes/prism-tomorrow.css";
+import { AppContext } from "../../context/revoltjs/RevoltClient";
 
-import MarkdownKatex from "@traptitech/markdown-it-katex";
-import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
+import { generateEmoji } from "../common/Emoji";
 
-// @ts-ignore
-import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
-// @ts-ignore
-import MarkdownSup from "markdown-it-sup";
-// @ts-ignore
-import MarkdownSub from "markdown-it-sub";
+import { emojiDictionary } from "../../assets/emojis";
+import { MarkdownProps } from "./Markdown";
 
 // TODO: global.d.ts file for defining globals
 declare global {
-    interface Window {
-        copycode: (element: HTMLDivElement) => void
-    }
+	interface Window {
+		copycode: (element: HTMLDivElement) => void;
+	}
 }
 
-
 // Handler for code block copy.
 if (typeof window !== "undefined") {
-    window.copycode = function(element: HTMLDivElement) {
-        try {
-            let code = element.parentElement?.parentElement?.children[1];
-            if (code) {
-                navigator.clipboard.writeText(code.textContent?.trim() ?? '');
-            }
-        } catch (e) {}
-    };
+	window.copycode = function (element: HTMLDivElement) {
+		try {
+			let code = element.parentElement?.parentElement?.children[1];
+			if (code) {
+				navigator.clipboard.writeText(code.textContent?.trim() ?? "");
+			}
+		} catch (e) {}
+	};
 }
 
 export const md: MarkdownIt = MarkdownIt({
-    breaks: true,
-    linkify: true,
-    highlight: (str, lang) => {
-        let v = Prism.languages[lang];
-        if (v) {
-            let out = Prism.highlight(str, v, lang);
-            return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
-        }
-        
-        return `<pre class="code"><code>${md.utils.escapeHtml(str)}</code></pre>`;
-    }
+	breaks: true,
+	linkify: true,
+	highlight: (str, lang) => {
+		let v = Prism.languages[lang];
+		if (v) {
+			let out = Prism.highlight(str, v, lang);
+			return `<pre class="code"><div class="lang"><div onclick="copycode(this)">${lang}</div></div><code class="language-${lang}">${out}</code></pre>`;
+		}
+
+		return `<pre class="code"><code>${md.utils.escapeHtml(
+			str,
+		)}</code></pre>`;
+	},
 })
-.disable("image")
-.use(MarkdownEmoji, { defs: emojiDictionary })
-.use(MarkdownSpoilers)
-.use(MarkdownSup)
-.use(MarkdownSub)
-.use(MarkdownKatex, {
-    throwOnError: false,
-    maxExpand: 0
-});
+	.disable("image")
+	.use(MarkdownEmoji, { defs: emojiDictionary })
+	.use(MarkdownSpoilers)
+	.use(MarkdownSup)
+	.use(MarkdownSub)
+	.use(MarkdownKatex, {
+		throwOnError: false,
+		maxExpand: 0,
+	});
 
 // ? Force links to open _blank.
 // From: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
 const defaultRender =
-    md.renderer.rules.link_open ||
-    function(tokens, idx, options, _env, self) {
-        return self.renderToken(tokens, idx, options);
-    };
+	md.renderer.rules.link_open ||
+	function (tokens, idx, options, _env, self) {
+		return self.renderToken(tokens, idx, options);
+	};
 
 // TODO: global.d.ts file for defining globals
 declare global {
-    interface Window {
-        internalHandleURL: (element: HTMLAnchorElement) => void
-    }
+	interface Window {
+		internalHandleURL: (element: HTMLAnchorElement) => void;
+	}
 }
 
 // Handler for internal links, pushes events to React using magic.
 if (typeof window !== "undefined") {
-    window.internalHandleURL = function(element: HTMLAnchorElement) {
-        const url = new URL(element.href, location.href);
-        const pathname = url.pathname;
-
-        if (pathname.startsWith("/@")) {
-            internalEmit("Intermediate", "openProfile", pathname.substr(2));
-        } else {
-            internalEmit("Intermediate", "navigate", pathname);
-        }
-    };
+	window.internalHandleURL = function (element: HTMLAnchorElement) {
+		const url = new URL(element.href, location.href);
+		const pathname = url.pathname;
+
+		if (pathname.startsWith("/@")) {
+			internalEmit("Intermediate", "openProfile", pathname.substr(2));
+		} else {
+			internalEmit("Intermediate", "navigate", pathname);
+		}
+	};
 }
 
-md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
-    let internal;
-    const hIndex = tokens[idx].attrIndex("href");
-    if (hIndex >= 0) {
-        try {
-            // For internal links, we should use our own handler to use react-router history.
-            // @ts-ignore
-            const href = tokens[idx].attrs[hIndex][1];
-            const url = new URL(href, location.href);
-
-            if (url.hostname === location.hostname) {
-                internal = true;
-                // I'm sorry.
-                tokens[idx].attrPush([
-                    "onclick",
-                    "internalHandleURL(this); return false"
-                ]);
-
-                if (url.pathname.startsWith("/@")) {
-                    tokens[idx].attrPush(["data-type", "mention"]);
-                }
-            }
-        } catch (err) {
-            // Ignore the error, treat as normal link.
-        }
-    }
-
-    if (!internal) {
-        // Add target=_blank for external links.
-        const aIndex = tokens[idx].attrIndex("target");
-
-        if (aIndex < 0) {
-            tokens[idx].attrPush(["target", "_blank"]);
-        } else {
-            try {
-                // @ts-ignore
-                tokens[idx].attrs[aIndex][1] = "_blank";
-            } catch (_) {}
-        }
-    }
-
-    return defaultRender(tokens, idx, options, env, self);
+md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
+	let internal;
+	const hIndex = tokens[idx].attrIndex("href");
+	if (hIndex >= 0) {
+		try {
+			// For internal links, we should use our own handler to use react-router history.
+			// @ts-ignore
+			const href = tokens[idx].attrs[hIndex][1];
+			const url = new URL(href, location.href);
+
+			if (url.hostname === location.hostname) {
+				internal = true;
+				// I'm sorry.
+				tokens[idx].attrPush([
+					"onclick",
+					"internalHandleURL(this); return false",
+				]);
+
+				if (url.pathname.startsWith("/@")) {
+					tokens[idx].attrPush(["data-type", "mention"]);
+				}
+			}
+		} catch (err) {
+			// Ignore the error, treat as normal link.
+		}
+	}
+
+	if (!internal) {
+		// Add target=_blank for external links.
+		const aIndex = tokens[idx].attrIndex("target");
+
+		if (aIndex < 0) {
+			tokens[idx].attrPush(["target", "_blank"]);
+		} else {
+			try {
+				// @ts-ignore
+				tokens[idx].attrs[aIndex][1] = "_blank";
+			} catch (_) {}
+		}
+	}
+
+	return defaultRender(tokens, idx, options, env, self);
 };
 
-md.renderer.rules.emoji = function(token, idx) {
-    return generateEmoji(token[idx].content);
+md.renderer.rules.emoji = function (token, idx) {
+	return generateEmoji(token[idx].content);
 };
 
 const RE_TWEMOJI = /:(\w+):/g;
 
 export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
-    const client = useContext(AppContext);
-    if (typeof content === "undefined") return null;
-    if (content.length === 0) return null;
-
-    // We replace the message with the mention at the time of render.
-    // We don't care if the mention changes.
-    let newContent = content.replace(
-        RE_MENTIONS,
-        (sub: string, ...args: any[]) => {
-            const id = args[0],
-                user = client.users.get(id);
-
-            if (user) {
-                return `[@${user.username}](/@${id})`;
-            }
-
-            return sub;
-        }
-    );
-
-    const useLargeEmojis = disallowBigEmoji ? false : content.replace(RE_TWEMOJI, '').trim().length === 0;
-
-    return (
-        <span
-            className={styles.markdown}
-            dangerouslySetInnerHTML={{
-                __html: md.render(newContent)
-            }}
-            data-large-emojis={useLargeEmojis}
-            onClick={ev => {
-                if (ev.target) {
-                    let element = ev.currentTarget;
-                    if (element.classList.contains("spoiler")) {
-                        element.classList.add("shown");
-                    }
-                }
-            }}
-        />
-    );
+	const client = useContext(AppContext);
+	if (typeof content === "undefined") return null;
+	if (content.length === 0) return null;
+
+	// We replace the message with the mention at the time of render.
+	// We don't care if the mention changes.
+	let newContent = content.replace(
+		RE_MENTIONS,
+		(sub: string, ...args: any[]) => {
+			const id = args[0],
+				user = client.users.get(id);
+
+			if (user) {
+				return `[@${user.username}](/@${id})`;
+			}
+
+			return sub;
+		},
+	);
+
+	const useLargeEmojis = disallowBigEmoji
+		? false
+		: content.replace(RE_TWEMOJI, "").trim().length === 0;
+
+	return (
+		<span
+			className={styles.markdown}
+			dangerouslySetInnerHTML={{
+				__html: md.render(newContent),
+			}}
+			data-large-emojis={useLargeEmojis}
+			onClick={(ev) => {
+				if (ev.target) {
+					let element = ev.currentTarget;
+					if (element.classList.contains("spoiler")) {
+						element.classList.add("shown");
+					}
+				}
+			}}
+		/>
+	);
 }
diff --git a/src/components/navigation/BottomNavigation.tsx b/src/components/navigation/BottomNavigation.tsx
index f2d2482ce6ff038323482b5cb3f81c1678769dbf..fd4629d4d6822b11135b8ad7f22a579967231e18 100644
--- a/src/components/navigation/BottomNavigation.tsx
+++ b/src/components/navigation/BottomNavigation.tsx
@@ -1,88 +1,96 @@
-import IconButton from "../ui/IconButton";
-import UserIcon from "../common/user/UserIcon";
-import styled, { css } from "styled-components";
-import { useSelf } from "../../context/revoltjs/hooks";
+import { Message, Group } from "@styled-icons/boxicons-solid";
 import { useHistory, useLocation } from "react-router";
+import styled, { css } from "styled-components";
+
 import ConditionalLink from "../../lib/ConditionalLink";
-import { Message, Group } from "@styled-icons/boxicons-solid";
-import { LastOpened } from "../../redux/reducers/last_opened";
+
 import { connectState } from "../../redux/connector";
+import { LastOpened } from "../../redux/reducers/last_opened";
+
+import { useSelf } from "../../context/revoltjs/hooks";
+
+import UserIcon from "../common/user/UserIcon";
+import IconButton from "../ui/IconButton";
 
 const NavigationBase = styled.div`
-    z-index: 100;
-    height: 50px;
-    display: flex;
-    background: var(--secondary-background);
+	z-index: 100;
+	height: 50px;
+	display: flex;
+	background: var(--secondary-background);
 `;
 
 const Button = styled.a<{ active: boolean }>`
-    flex: 1;
+	flex: 1;
 
-    > a, > div, > a > div {
-        width: 100%;
-        height: 100%;
-    }
+	> a,
+	> div,
+	> a > div {
+		width: 100%;
+		height: 100%;
+	}
 
-    ${ props => props.active && css`
-        background: var(--hover);
-    ` }
+	${(props) =>
+		props.active &&
+		css`
+			background: var(--hover);
+		`}
 `;
 
 interface Props {
-    lastOpened: LastOpened
+	lastOpened: LastOpened;
 }
 
 export function BottomNavigation({ lastOpened }: Props) {
-    const user = useSelf();
-    const history = useHistory();
-    const path = useLocation().pathname;
+	const user = useSelf();
+	const history = useHistory();
+	const path = useLocation().pathname;
+
+	const channel_id = lastOpened["home"];
 
-    const channel_id = lastOpened['home'];
+	const friendsActive = path.startsWith("/friends");
+	const settingsActive = path.startsWith("/settings");
+	const homeActive = !(friendsActive || settingsActive);
 
-    const friendsActive = path.startsWith("/friends");
-    const settingsActive = path.startsWith("/settings");
-    const homeActive = !(friendsActive || settingsActive);
+	return (
+		<NavigationBase>
+			<Button active={homeActive}>
+				<IconButton
+					onClick={() => {
+						if (settingsActive) {
+							if (history.length > 0) {
+								history.goBack();
+							}
+						}
 
-    return (
-        <NavigationBase>
-            <Button active={homeActive}>
-                <IconButton
-                    onClick={() => {
-                        if (settingsActive) {
-                            if (history.length > 0) {
-                                history.goBack();
-                            }
-                        }
-                        
-                        if (channel_id) {
-                            history.push(`/channel/${channel_id}`);
-                        } else {
-                            history.push('/');
-                        }
-                    }}>
-                    <Message size={24} />
-                </IconButton>
-            </Button>
-            <Button active={friendsActive}>
-                <ConditionalLink active={friendsActive} to="/friends">
-                    <IconButton>
-                        <Group size={25} />
-                    </IconButton>
-                </ConditionalLink>
-            </Button>
-            <Button active={settingsActive}>
-                <ConditionalLink active={settingsActive} to="/settings">
-                    <IconButton>
-                        <UserIcon target={user} size={26} status={true} />
-                    </IconButton>
-                </ConditionalLink>
-            </Button>
-        </NavigationBase>
-    );
+						if (channel_id) {
+							history.push(`/channel/${channel_id}`);
+						} else {
+							history.push("/");
+						}
+					}}>
+					<Message size={24} />
+				</IconButton>
+			</Button>
+			<Button active={friendsActive}>
+				<ConditionalLink active={friendsActive} to="/friends">
+					<IconButton>
+						<Group size={25} />
+					</IconButton>
+				</ConditionalLink>
+			</Button>
+			<Button active={settingsActive}>
+				<ConditionalLink active={settingsActive} to="/settings">
+					<IconButton>
+						<UserIcon target={user} size={26} status={true} />
+					</IconButton>
+				</ConditionalLink>
+			</Button>
+		</NavigationBase>
+	);
 }
 
-export default connectState(BottomNavigation, state => {
-    return {
-        lastOpened: state.lastOpened
-    }
+export default connectState(BottomNavigation, (state) => {
+	return {
+		lastOpened: state.lastOpened,
+	};
 });
diff --git a/src/components/navigation/LeftSidebar.tsx b/src/components/navigation/LeftSidebar.tsx
index 6852a848c9b506463a1320456dcd88a94f02de4f..ccc675d2e30461f683168273c87844bfabc36daf 100644
--- a/src/components/navigation/LeftSidebar.tsx
+++ b/src/components/navigation/LeftSidebar.tsx
@@ -1,32 +1,32 @@
 import { Route, Switch } from "react-router";
-import SidebarBase from "./SidebarBase";
 
+import SidebarBase from "./SidebarBase";
+import HomeSidebar from "./left/HomeSidebar";
 import ServerListSidebar from "./left/ServerListSidebar";
 import ServerSidebar from "./left/ServerSidebar";
-import HomeSidebar from "./left/HomeSidebar";
 
 export default function LeftSidebar() {
-    return (
-        <SidebarBase>
-            <Switch>
-                <Route path="/settings" />
-                <Route path="/server/:server/channel/:channel">
-                    <ServerListSidebar />
-                    <ServerSidebar />
-                </Route>
-                <Route path="/server/:server">
-                    <ServerListSidebar />
-                    <ServerSidebar />
-                </Route>
-                <Route path="/channel/:channel">
-                    <ServerListSidebar />
-                    <HomeSidebar />
-                </Route>
-                <Route path="/">
-                    <ServerListSidebar />
-                    <HomeSidebar />
-                </Route>
-            </Switch>
-        </SidebarBase>
-    );
-};
+	return (
+		<SidebarBase>
+			<Switch>
+				<Route path="/settings" />
+				<Route path="/server/:server/channel/:channel">
+					<ServerListSidebar />
+					<ServerSidebar />
+				</Route>
+				<Route path="/server/:server">
+					<ServerListSidebar />
+					<ServerSidebar />
+				</Route>
+				<Route path="/channel/:channel">
+					<ServerListSidebar />
+					<HomeSidebar />
+				</Route>
+				<Route path="/">
+					<ServerListSidebar />
+					<HomeSidebar />
+				</Route>
+			</Switch>
+		</SidebarBase>
+	);
+}
diff --git a/src/components/navigation/RightSidebar.tsx b/src/components/navigation/RightSidebar.tsx
index 45371a8fa4eea465cc45c2dc74294e17606eaa1f..b1d6adfa826670321e12baeaccb899f57c7f4dd1 100644
--- a/src/components/navigation/RightSidebar.tsx
+++ b/src/components/navigation/RightSidebar.tsx
@@ -1,19 +1,19 @@
 import { Route, Switch } from "react-router";
-import SidebarBase from "./SidebarBase";
 
+import SidebarBase from "./SidebarBase";
 import MemberSidebar from "./right/MemberSidebar";
 
 export default function RightSidebar() {
-    return (
-        <SidebarBase>
-            <Switch>
-                <Route path="/server/:server/channel/:channel">
-                    <MemberSidebar />
-                </Route>
-                <Route path="/channel/:channel">
-                    <MemberSidebar />
-                </Route>
-            </Switch>
-        </SidebarBase>
-    );
-};
+	return (
+		<SidebarBase>
+			<Switch>
+				<Route path="/server/:server/channel/:channel">
+					<MemberSidebar />
+				</Route>
+				<Route path="/channel/:channel">
+					<MemberSidebar />
+				</Route>
+			</Switch>
+		</SidebarBase>
+	);
+}
diff --git a/src/components/navigation/SidebarBase.tsx b/src/components/navigation/SidebarBase.tsx
index 00c6f66f8027391bc733e68d349a58afbf273a04..8d5cd607f5c746c524d4c55b61f070c0cde94d68 100644
--- a/src/components/navigation/SidebarBase.tsx
+++ b/src/components/navigation/SidebarBase.tsx
@@ -1,34 +1,38 @@
 import styled, { css } from "styled-components";
+
 import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
 
 export default styled.div`
-    height: 100%;
-    display: flex;
-    user-select: none;
-    flex-direction: row;
-    align-items: stretch;
+	height: 100%;
+	display: flex;
+	user-select: none;
+	flex-direction: row;
+	align-items: stretch;
 `;
 
 export const GenericSidebarBase = styled.div<{ padding?: boolean }>`
-    height: 100%;
-    width: 240px;
-    display: flex;
-    flex-shrink: 0;
-    flex-direction: column;
-    background: var(--secondary-background);
-    border-end-start-radius: 8px;
+	height: 100%;
+	width: 240px;
+	display: flex;
+	flex-shrink: 0;
+	flex-direction: column;
+	background: var(--secondary-background);
+	border-end-start-radius: 8px;
 
-    ${ props => props.padding && isTouchscreenDevice && css`
-        padding-bottom: 50px;
-    ` }
+	${(props) =>
+		props.padding &&
+		isTouchscreenDevice &&
+		css`
+			padding-bottom: 50px;
+		`}
 `;
 
 export const GenericSidebarList = styled.div`
-    padding: 6px;
-    flex-grow: 1;
-    overflow-y: scroll;
+	padding: 6px;
+	flex-grow: 1;
+	overflow-y: scroll;
 
-    > img {
-        width: 100%;
-    }
+	> img {
+		width: 100%;
+	}
 `;
diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx
index 176f5f809f0a63ef9495c3a789094ae865f2a49b..5da41cf5e9eb78868342cae83db08af87c50f4bb 100644
--- a/src/components/navigation/items/ButtonItem.tsx
+++ b/src/components/navigation/items/ButtonItem.tsx
@@ -1,158 +1,223 @@
-import classNames from 'classnames';
-import styles from "./Item.module.scss";
-import Tooltip from '../../common/Tooltip';
-import IconButton from '../../ui/IconButton';
-import { Localizer, Text } from "preact-i18n";
 import { X, Crown } from "@styled-icons/boxicons-regular";
-import { Children } from "../../../types/Preact";
-import UserIcon from '../../common/user/UserIcon';
-import ChannelIcon from '../../common/ChannelIcon';
-import UserStatus from '../../common/user/UserStatus';
-import { attachContextMenu } from 'preact-context-menu';
 import { Channels, Users } from "revolt.js/dist/api/objects";
+
+import styles from "./Item.module.scss";
+import classNames from "classnames";
+import { attachContextMenu } from "preact-context-menu";
+import { Localizer, Text } from "preact-i18n";
+
 import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
-import { useIntermediate } from '../../../context/intermediate/Intermediate';
-import { stopPropagation } from '../../../lib/stopPropagation';
+import { stopPropagation } from "../../../lib/stopPropagation";
 
-type CommonProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
-    active?: boolean
-    alert?: 'unread' | 'mention'
-    alertCount?: number
-}
+import { useIntermediate } from "../../../context/intermediate/Intermediate";
+
+import ChannelIcon from "../../common/ChannelIcon";
+import Tooltip from "../../common/Tooltip";
+import UserIcon from "../../common/user/UserIcon";
+import UserStatus from "../../common/user/UserStatus";
+import IconButton from "../../ui/IconButton";
+
+import { Children } from "../../../types/Preact";
+
+type CommonProps = Omit<
+	JSX.HTMLAttributes<HTMLDivElement>,
+	"children" | "as"
+> & {
+	active?: boolean;
+	alert?: "unread" | "mention";
+	alertCount?: number;
+};
 
 type UserProps = CommonProps & {
-    user: Users.User,
-    context?: Channels.Channel,
-    channel?: Channels.DirectMessageChannel
-}
+	user: Users.User;
+	context?: Channels.Channel;
+	channel?: Channels.DirectMessageChannel;
+};
 
 export function UserButton(props: UserProps) {
-    const { active, alert, alertCount, user, context, channel, ...divProps } = props;
-    const { openScreen } = useIntermediate();
-
-    return (
-        <div {...divProps}
-            className={classNames(styles.item, styles.user)}
-            data-active={active}
-            data-alert={typeof alert === 'string'}
-            data-online={typeof channel !== 'undefined' || (user.online && user.status?.presence !== Users.Presence.Invisible)}
-            onContextMenu={attachContextMenu('Menu', {
-                user: user._id,
-                channel: channel?._id,
-                unread: alert,
-                contextualChannel: context?._id
-            })}>
-            <UserIcon className={styles.avatar} target={user} size={32} status />
-            <div className={styles.name}>
-                <div>{user.username}</div>
-                {
-                    <div className={styles.subText}>
-                        { channel?.last_message && alert ? (
-                            channel.last_message.short
-                        ) : (
-                            <UserStatus user={user} />
-                        ) }
-                    </div>
-                }
-            </div>
-            <div className={styles.button}>
-                { context?.channel_type === "Group" &&
-                    context.owner === user._id && (
-                        <Localizer>
-                            <Tooltip
-                                content={
-                                    <Text id="app.main.groups.owner" />
-                                }
-                            >
-                                <Crown size={20} />
-                            </Tooltip>
-                        </Localizer>
-                )}
-                {alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
-                { !isTouchscreenDevice && channel &&
-                    <IconButton className={styles.icon}
-                        onClick={e => stopPropagation(e) && openScreen({ id: 'special_prompt', type: 'close_dm', target: channel })}>
-                        <X size={24} />
-                    </IconButton>
-                }
-            </div>
-        </div>
-    )
+	const { active, alert, alertCount, user, context, channel, ...divProps } =
+		props;
+	const { openScreen } = useIntermediate();
+
+	return (
+		<div
+			{...divProps}
+			className={classNames(styles.item, styles.user)}
+			data-active={active}
+			data-alert={typeof alert === "string"}
+			data-online={
+				typeof channel !== "undefined" ||
+				(user.online &&
+					user.status?.presence !== Users.Presence.Invisible)
+			}
+			onContextMenu={attachContextMenu("Menu", {
+				user: user._id,
+				channel: channel?._id,
+				unread: alert,
+				contextualChannel: context?._id,
+			})}>
+			<UserIcon
+				className={styles.avatar}
+				target={user}
+				size={32}
+				status
+			/>
+			<div className={styles.name}>
+				<div>{user.username}</div>
+				{
+					<div className={styles.subText}>
+						{channel?.last_message && alert ? (
+							channel.last_message.short
+						) : (
+							<UserStatus user={user} />
+						)}
+					</div>
+				}
+			</div>
+			<div className={styles.button}>
+				{context?.channel_type === "Group" &&
+					context.owner === user._id && (
+						<Localizer>
+							<Tooltip
+								content={<Text id="app.main.groups.owner" />}>
+								<Crown size={20} />
+							</Tooltip>
+						</Localizer>
+					)}
+				{alert && (
+					<div className={styles.alert} data-style={alert}>
+						{alertCount}
+					</div>
+				)}
+				{!isTouchscreenDevice && channel && (
+					<IconButton
+						className={styles.icon}
+						onClick={(e) =>
+							stopPropagation(e) &&
+							openScreen({
+								id: "special_prompt",
+								type: "close_dm",
+								target: channel,
+							})
+						}>
+						<X size={24} />
+					</IconButton>
+				)}
+			</div>
+		</div>
+	);
 }
 
 type ChannelProps = CommonProps & {
-    channel: Channels.Channel & { unread?: string },
-    user?: Users.User
-    compact?: boolean
-}
+	channel: Channels.Channel & { unread?: string };
+	user?: Users.User;
+	compact?: boolean;
+};
 
 export function ChannelButton(props: ChannelProps) {
-    const { active, alert, alertCount, channel, user, compact, ...divProps } = props;
-
-    if (channel.channel_type === 'SavedMessages') throw "Invalid channel type.";
-    if (channel.channel_type === 'DirectMessage') {
-        if (typeof user === 'undefined') throw "No user provided.";
-        return <UserButton {...{ active, alert, channel, user }} />
-    }
-
-    const { openScreen } = useIntermediate();
-
-    return (
-        <div {...divProps}
-            data-active={active}
-            data-alert={typeof alert === 'string'}
-            aria-label={{}} /*FIXME: ADD ARIA LABEL*/
-            className={classNames(styles.item, { [styles.compact]: compact })}
-            onContextMenu={attachContextMenu('Menu', { channel: channel._id, unread: typeof channel.unread !== 'undefined' })}>
-            <ChannelIcon className={styles.avatar} target={channel} size={compact ? 24 : 32} />
-            <div className={styles.name}>
-                <div>{channel.name}</div>
-                { channel.channel_type === 'Group' &&
-                    <div className={styles.subText}>
-                        {(channel.last_message && alert) ? (
-                            channel.last_message.short
-                        ) : (
-                            <Text
-                                id="quantities.members"
-                                plural={channel.recipients.length}
-                                fields={{ count: channel.recipients.length }}
-                            />
-                        )}
-                    </div>
-                }
-            </div>
-            <div className={styles.button}>
-                {alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
-                {!isTouchscreenDevice && channel.channel_type === "Group" && (
-                    <IconButton
-                        className={styles.icon}
-                        onClick={() => openScreen({ id: 'special_prompt', type: 'leave_group', target: channel })}>
-                        <X size={24} />
-                    </IconButton>
-                )}
-            </div>
-        </div>
-    )
+	const { active, alert, alertCount, channel, user, compact, ...divProps } =
+		props;
+
+	if (channel.channel_type === "SavedMessages") throw "Invalid channel type.";
+	if (channel.channel_type === "DirectMessage") {
+		if (typeof user === "undefined") throw "No user provided.";
+		return <UserButton {...{ active, alert, channel, user }} />;
+	}
+
+	const { openScreen } = useIntermediate();
+
+	return (
+		<div
+			{...divProps}
+			data-active={active}
+			data-alert={typeof alert === "string"}
+			aria-label={{}} /*FIXME: ADD ARIA LABEL*/
+			className={classNames(styles.item, { [styles.compact]: compact })}
+			onContextMenu={attachContextMenu("Menu", {
+				channel: channel._id,
+				unread: typeof channel.unread !== "undefined",
+			})}>
+			<ChannelIcon
+				className={styles.avatar}
+				target={channel}
+				size={compact ? 24 : 32}
+			/>
+			<div className={styles.name}>
+				<div>{channel.name}</div>
+				{channel.channel_type === "Group" && (
+					<div className={styles.subText}>
+						{channel.last_message && alert ? (
+							channel.last_message.short
+						) : (
+							<Text
+								id="quantities.members"
+								plural={channel.recipients.length}
+								fields={{ count: channel.recipients.length }}
+							/>
+						)}
+					</div>
+				)}
+			</div>
+			<div className={styles.button}>
+				{alert && (
+					<div className={styles.alert} data-style={alert}>
+						{alertCount}
+					</div>
+				)}
+				{!isTouchscreenDevice && channel.channel_type === "Group" && (
+					<IconButton
+						className={styles.icon}
+						onClick={() =>
+							openScreen({
+								id: "special_prompt",
+								type: "leave_group",
+								target: channel,
+							})
+						}>
+						<X size={24} />
+					</IconButton>
+				)}
+			</div>
+		</div>
+	);
 }
 
 type ButtonProps = CommonProps & {
-    onClick?: () => void
-    children?: Children
-    className?: string
-    compact?: boolean
-}
+	onClick?: () => void;
+	children?: Children;
+	className?: string;
+	compact?: boolean;
+};
 
 export default function ButtonItem(props: ButtonProps) {
-    const { active, alert, alertCount, onClick, className, children, compact, ...divProps } = props;
-
-    return (
-        <div {...divProps}
-            className={classNames(styles.item, { [styles.compact]: compact, [styles.normal]: !compact }, className)}
-            onClick={onClick}
-            data-active={active}
-            data-alert={typeof alert === 'string'}>
-            <div className={styles.content}>{ children }</div>
-                {alert && <div className={styles.alert} data-style={alert}>{ alertCount }</div>}
-        </div>
-    )
+	const {
+		active,
+		alert,
+		alertCount,
+		onClick,
+		className,
+		children,
+		compact,
+		...divProps
+	} = props;
+
+	return (
+		<div
+			{...divProps}
+			className={classNames(
+				styles.item,
+				{ [styles.compact]: compact, [styles.normal]: !compact },
+				className,
+			)}
+			onClick={onClick}
+			data-active={active}
+			data-alert={typeof alert === "string"}>
+			<div className={styles.content}>{children}</div>
+			{alert && (
+				<div className={styles.alert} data-style={alert}>
+					{alertCount}
+				</div>
+			)}
+		</div>
+	);
 }
diff --git a/src/components/navigation/items/ConnectionStatus.tsx b/src/components/navigation/items/ConnectionStatus.tsx
index 7b7a9b08bbfcd3f6c9e0374b110b771172b9f41b..667f2da5e7fd51c23d1a38f3f96805a05130b582 100644
--- a/src/components/navigation/items/ConnectionStatus.tsx
+++ b/src/components/navigation/items/ConnectionStatus.tsx
@@ -1,35 +1,40 @@
 import { Text } from "preact-i18n";
-import Banner from "../../ui/Banner";
 import { useContext } from "preact/hooks";
-import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
+
+import {
+	ClientStatus,
+	StatusContext,
+} from "../../../context/revoltjs/RevoltClient";
+
+import Banner from "../../ui/Banner";
 
 export default function ConnectionStatus() {
-    const status = useContext(StatusContext);
+	const status = useContext(StatusContext);
 
-    if (status === ClientStatus.OFFLINE) {
-        return (
-            <Banner>
-                <Text id="app.special.status.offline" />
-            </Banner>
-        );
-    } else if (status === ClientStatus.DISCONNECTED) {
-        return (
-            <Banner>
-                <Text id="app.special.status.disconnected" />
-            </Banner>
-        );
-    } else if (status === ClientStatus.CONNECTING) {
-        return (
-            <Banner>
-                <Text id="app.special.status.connecting" />
-            </Banner>
-        );
-    } else if (status === ClientStatus.RECONNECTING) {
-        return (
-            <Banner>
-                <Text id="app.special.status.reconnecting" />
-            </Banner>
-        );
-    }
-    return null;
+	if (status === ClientStatus.OFFLINE) {
+		return (
+			<Banner>
+				<Text id="app.special.status.offline" />
+			</Banner>
+		);
+	} else if (status === ClientStatus.DISCONNECTED) {
+		return (
+			<Banner>
+				<Text id="app.special.status.disconnected" />
+			</Banner>
+		);
+	} else if (status === ClientStatus.CONNECTING) {
+		return (
+			<Banner>
+				<Text id="app.special.status.connecting" />
+			</Banner>
+		);
+	} else if (status === ClientStatus.RECONNECTING) {
+		return (
+			<Banner>
+				<Text id="app.special.status.reconnecting" />
+			</Banner>
+		);
+	}
+	return null;
 }
diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx
index fc883f42aa86667b020c4b2f52c3d3a2cb5aa45f..a0de1380e4d5aec1a3671aab6308102c5434aae9 100644
--- a/src/components/navigation/left/HomeSidebar.tsx
+++ b/src/components/navigation/left/HomeSidebar.tsx
@@ -1,152 +1,193 @@
+import {
+	Home,
+	UserDetail,
+	Wrench,
+	Notepad,
+} from "@styled-icons/boxicons-solid";
+import { Link, Redirect, useLocation, useParams } from "react-router-dom";
+import { Channels } from "revolt.js/dist/api/objects";
+import { Users as UsersNS } from "revolt.js/dist/api/objects";
+
 import { Text } from "preact-i18n";
 import { useContext, useEffect } from "preact/hooks";
-import { Home, UserDetail, Wrench, Notepad } from "@styled-icons/boxicons-solid";
 
-import Category from '../../ui/Category';
-import { dispatch } from "../../../redux";
+import ConditionalLink from "../../../lib/ConditionalLink";
 import PaintCounter from "../../../lib/PaintCounter";
-import UserHeader from "../../common/user/UserHeader";
-import { Channels } from "revolt.js/dist/api/objects";
+import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
+
+import { dispatch } from "../../../redux";
 import { connectState } from "../../../redux/connector";
-import ConnectionStatus from '../items/ConnectionStatus';
 import { Unreads } from "../../../redux/reducers/unreads";
-import ConditionalLink from "../../../lib/ConditionalLink";
-import { mapChannelWithUnread, useUnreads } from "./common";
-import { Users as UsersNS } from 'revolt.js/dist/api/objects';
-import ButtonItem, { ChannelButton } from '../items/ButtonItem';
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
-import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
-import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
-import { Link, Redirect, useLocation, useParams } from "react-router-dom";
+
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { useDMs, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import {
+	useDMs,
+	useForceUpdate,
+	useUsers,
+} from "../../../context/revoltjs/hooks";
 
+import UserHeader from "../../common/user/UserHeader";
+import Category from "../../ui/Category";
 import placeholderSVG from "../items/placeholder.svg";
+import { mapChannelWithUnread, useUnreads } from "./common";
+
+import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
+import ButtonItem, { ChannelButton } from "../items/ButtonItem";
+import ConnectionStatus from "../items/ConnectionStatus";
 
 type Props = {
-    unreads: Unreads;
-}
+	unreads: Unreads;
+};
 
 function HomeSidebar(props: Props) {
-    const { pathname } = useLocation();
-    const client = useContext(AppContext);
-    const { channel } = useParams<{ channel: string }>();
-    const { openScreen } = useIntermediate();
-
-    const ctx = useForceUpdate();
-    const channels = useDMs(ctx);
-
-    const obj = channels.find(x => x?._id === channel);
-    if (channel && !obj) return <Redirect to="/" />;
-    if (obj) useUnreads({ ...props, channel: obj });
-
-    useEffect(() => {
-        if (!channel) return;
-
-        dispatch({
-            type: 'LAST_OPENED_SET',
-            parent: 'home',
-            child: channel
-        });
-    }, [ channel ]);
-
-    const channelsArr = channels
-        .filter(x => x.channel_type !== 'SavedMessages')
-        .map(x => mapChannelWithUnread(x, props.unreads));
-
-    const users = useUsers(
-        (channelsArr as (Channels.DirectMessageChannel | Channels.GroupChannel)[])
-            .reduce((prev: any, cur) => [ ...prev, ...cur.recipients ], [])
-    , ctx);
-
-    channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
-
-    return (
-        <GenericSidebarBase padding>
-            <UserHeader user={client.user!} />
-            <ConnectionStatus />
-            <GenericSidebarList>
-                {!isTouchscreenDevice && (
-                    <>
-                        <ConditionalLink active={pathname === "/"} to="/">
-                            <ButtonItem active={pathname === "/"}>
-                                <Home size={20} />
-                                <span><Text id="app.navigation.tabs.home" /></span>
-                            </ButtonItem>
-                        </ConditionalLink>
-                        <ConditionalLink active={pathname === "/friends"} to="/friends">
-                            <ButtonItem
-                                active={pathname === "/friends"}
-                                alert={
-                                    typeof users.find(
-                                        user =>
-                                            user?.relationship ===
-                                            UsersNS.Relationship.Incoming
-                                    ) !== "undefined" ? 'unread' : undefined
-                                }
-                            >
-                                <UserDetail size={20} />
-                                <span><Text id="app.navigation.tabs.friends" /></span>
-                            </ButtonItem>
-                        </ConditionalLink>
-                    </>
-                )}
-                <ConditionalLink active={obj?.channel_type === "SavedMessages"} to="/open/saved">
-                    <ButtonItem active={obj?.channel_type === "SavedMessages"}>
-                        <Notepad size={20} />
-                        <span><Text id="app.navigation.tabs.saved" /></span>
-                    </ButtonItem>
-                </ConditionalLink>
-                {import.meta.env.DEV && (
-                    <Link to="/dev">
-                        <ButtonItem active={pathname === "/dev"}>
-                            <Wrench size={20} />
-                            <span><Text id="app.navigation.tabs.dev" /></span>
-                        </ButtonItem>
-                    </Link>
-                )}
-                <Category
-                    text={<Text id="app.main.categories.conversations" />}
-                    action={() => openScreen({ id: "special_input", type: "create_group" })} />
-                {channelsArr.length === 0 && <img src={placeholderSVG} />}
-                {channelsArr.map(x => {
-                    let user;
-                    if (x.channel_type === 'DirectMessage') {
-                        if (!x.active) return null;
-
-                        let recipient = client.channels.getRecipient(x._id);
-                        user = users.find(x => x?._id === recipient);
-
-                        if (!user) {
-                            console.warn(`Skipped DM ${x._id} because user was missing.`);
-                            return null;
-                        }
-                    }
-                    
-                    return (
-                        <ConditionalLink active={x._id === channel} to={`/channel/${x._id}`}>
-                            <ChannelButton
-                                user={user}
-                                channel={x}
-                                alert={x.unread}
-                                alertCount={x.alertCount}
-                                active={x._id === channel}
-                            />
-                        </ConditionalLink>
-                    );
-                })}
-                <PaintCounter />
-            </GenericSidebarList>
-        </GenericSidebarBase>
-    );
-};
+	const { pathname } = useLocation();
+	const client = useContext(AppContext);
+	const { channel } = useParams<{ channel: string }>();
+	const { openScreen } = useIntermediate();
+
+	const ctx = useForceUpdate();
+	const channels = useDMs(ctx);
+
+	const obj = channels.find((x) => x?._id === channel);
+	if (channel && !obj) return <Redirect to="/" />;
+	if (obj) useUnreads({ ...props, channel: obj });
+
+	useEffect(() => {
+		if (!channel) return;
+
+		dispatch({
+			type: "LAST_OPENED_SET",
+			parent: "home",
+			child: channel,
+		});
+	}, [channel]);
+
+	const channelsArr = channels
+		.filter((x) => x.channel_type !== "SavedMessages")
+		.map((x) => mapChannelWithUnread(x, props.unreads));
+
+	const users = useUsers(
+		(
+			channelsArr as (
+				| Channels.DirectMessageChannel
+				| Channels.GroupChannel
+			)[]
+		).reduce((prev: any, cur) => [...prev, ...cur.recipients], []),
+		ctx,
+	);
+
+	channelsArr.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
+
+	return (
+		<GenericSidebarBase padding>
+			<UserHeader user={client.user!} />
+			<ConnectionStatus />
+			<GenericSidebarList>
+				{!isTouchscreenDevice && (
+					<>
+						<ConditionalLink active={pathname === "/"} to="/">
+							<ButtonItem active={pathname === "/"}>
+								<Home size={20} />
+								<span>
+									<Text id="app.navigation.tabs.home" />
+								</span>
+							</ButtonItem>
+						</ConditionalLink>
+						<ConditionalLink
+							active={pathname === "/friends"}
+							to="/friends">
+							<ButtonItem
+								active={pathname === "/friends"}
+								alert={
+									typeof users.find(
+										(user) =>
+											user?.relationship ===
+											UsersNS.Relationship.Incoming,
+									) !== "undefined"
+										? "unread"
+										: undefined
+								}>
+								<UserDetail size={20} />
+								<span>
+									<Text id="app.navigation.tabs.friends" />
+								</span>
+							</ButtonItem>
+						</ConditionalLink>
+					</>
+				)}
+				<ConditionalLink
+					active={obj?.channel_type === "SavedMessages"}
+					to="/open/saved">
+					<ButtonItem active={obj?.channel_type === "SavedMessages"}>
+						<Notepad size={20} />
+						<span>
+							<Text id="app.navigation.tabs.saved" />
+						</span>
+					</ButtonItem>
+				</ConditionalLink>
+				{import.meta.env.DEV && (
+					<Link to="/dev">
+						<ButtonItem active={pathname === "/dev"}>
+							<Wrench size={20} />
+							<span>
+								<Text id="app.navigation.tabs.dev" />
+							</span>
+						</ButtonItem>
+					</Link>
+				)}
+				<Category
+					text={<Text id="app.main.categories.conversations" />}
+					action={() =>
+						openScreen({
+							id: "special_input",
+							type: "create_group",
+						})
+					}
+				/>
+				{channelsArr.length === 0 && <img src={placeholderSVG} />}
+				{channelsArr.map((x) => {
+					let user;
+					if (x.channel_type === "DirectMessage") {
+						if (!x.active) return null;
+
+						let recipient = client.channels.getRecipient(x._id);
+						user = users.find((x) => x?._id === recipient);
+
+						if (!user) {
+							console.warn(
+								`Skipped DM ${x._id} because user was missing.`,
+							);
+							return null;
+						}
+					}
+
+					return (
+						<ConditionalLink
+							active={x._id === channel}
+							to={`/channel/${x._id}`}>
+							<ChannelButton
+								user={user}
+								channel={x}
+								alert={x.unread}
+								alertCount={x.alertCount}
+								active={x._id === channel}
+							/>
+						</ConditionalLink>
+					);
+				})}
+				<PaintCounter />
+			</GenericSidebarList>
+		</GenericSidebarBase>
+	);
+}
 
 export default connectState(
-    HomeSidebar,
-    state => {
-        return {
-            unreads: state.unreads
-        };
-    },
-    true
+	HomeSidebar,
+	(state) => {
+		return {
+			unreads: state.unreads,
+		};
+	},
+	true,
 );
diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx
index 3ba5a986c5beb3de1295577f9cd8d4a74099025b..f75ac8e14f05ff565b091b0f94629a9c1fd74b9f 100644
--- a/src/components/navigation/left/ServerListSidebar.tsx
+++ b/src/components/navigation/left/ServerListSidebar.tsx
@@ -1,257 +1,298 @@
-import Tooltip from "../../common/Tooltip";
-import IconButton from "../../ui/IconButton";
-import LineDivider from "../../ui/LineDivider";
-import { mapChannelWithUnread } from "./common";
-import styled, { css } from "styled-components";
-import ServerIcon from "../../common/ServerIcon";
-import { Children } from "../../../types/Preact";
-import UserIcon from "../../common/user/UserIcon";
-import PaintCounter from "../../../lib/PaintCounter";
 import { Plus } from "@styled-icons/boxicons-regular";
-import { connectState } from "../../../redux/connector";
 import { useLocation, useParams } from "react-router-dom";
-import { Unreads } from "../../../redux/reducers/unreads";
-import ConditionalLink from "../../../lib/ConditionalLink";
 import { Channel, Servers } from "revolt.js/dist/api/objects";
-import { LastOpened } from "../../../redux/reducers/last_opened";
+import styled, { css } from "styled-components";
+
+import { attachContextMenu, openContextMenu } from "preact-context-menu";
+
+import ConditionalLink from "../../../lib/ConditionalLink";
+import PaintCounter from "../../../lib/PaintCounter";
 import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
-import { attachContextMenu, openContextMenu } from 'preact-context-menu';
+
+import { connectState } from "../../../redux/connector";
+import { LastOpened } from "../../../redux/reducers/last_opened";
+import { Unreads } from "../../../redux/reducers/unreads";
+
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { useChannels, useForceUpdate, useSelf, useServers } from "../../../context/revoltjs/hooks";
-
-function Icon({ children, unread, size }: { children: Children, unread?: 'mention' | 'unread', size: number }) {
-    return (
-        <svg
-            width={size}
-            height={size}
-            aria-hidden="true"
-            viewBox="0 0 32 32"
-        >
-            <use href="#serverIndicator" />
-            <foreignObject x="0" y="0" width="32" height="32" mask={ unread ? "url(#server)" : undefined }>
-                { children }
-            </foreignObject>
-            {unread === 'unread' && (
-                <circle
-                    cx="27"
-                    cy="5"
-                    r="5"
-                    fill={"white"}
-                />
-            )}
-            {unread === 'mention' && (
-                <circle
-                    cx="27"
-                    cy="5"
-                    r="5"
-                    fill={"red"}
-                />
-            )}
-        </svg>
-    )
+import {
+	useChannels,
+	useForceUpdate,
+	useSelf,
+	useServers,
+} from "../../../context/revoltjs/hooks";
+
+import ServerIcon from "../../common/ServerIcon";
+import Tooltip from "../../common/Tooltip";
+import UserIcon from "../../common/user/UserIcon";
+import IconButton from "../../ui/IconButton";
+import LineDivider from "../../ui/LineDivider";
+import { mapChannelWithUnread } from "./common";
+
+import { Children } from "../../../types/Preact";
+
+function Icon({
+	children,
+	unread,
+	size,
+}: {
+	children: Children;
+	unread?: "mention" | "unread";
+	size: number;
+}) {
+	return (
+		<svg width={size} height={size} aria-hidden="true" viewBox="0 0 32 32">
+			<use href="#serverIndicator" />
+			<foreignObject
+				x="0"
+				y="0"
+				width="32"
+				height="32"
+				mask={unread ? "url(#server)" : undefined}>
+				{children}
+			</foreignObject>
+			{unread === "unread" && (
+				<circle cx="27" cy="5" r="5" fill={"white"} />
+			)}
+			{unread === "mention" && (
+				<circle cx="27" cy="5" r="5" fill={"red"} />
+			)}
+		</svg>
+	);
 }
 
 const ServersBase = styled.div`
-    width: 56px;
-    height: 100%;
-    display: flex;
-    flex-direction: column;
-
-    ${ isTouchscreenDevice && css`
-        padding-bottom: 50px;
-    ` }
+	width: 56px;
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+
+	${isTouchscreenDevice &&
+	css`
+		padding-bottom: 50px;
+	`}
 `;
 
 const ServerList = styled.div`
-    flex-grow: 1;
-    display: flex;
-    overflow-y: scroll;
-    padding-bottom: 48px;
-    flex-direction: column;
-    // border-right: 2px solid var(--accent);
-
-    scrollbar-width: none;
-
-    > :first-child > svg {
-        margin: 6px 0 6px 4px;
-    }
-
-    &::-webkit-scrollbar {
-        width: 0px;
-    }
+	flex-grow: 1;
+	display: flex;
+	overflow-y: scroll;
+	padding-bottom: 48px;
+	flex-direction: column;
+	// border-right: 2px solid var(--accent);
+
+	scrollbar-width: none;
+
+	> :first-child > svg {
+		margin: 6px 0 6px 4px;
+	}
+
+	&::-webkit-scrollbar {
+		width: 0px;
+	}
 `;
 
-const ServerEntry = styled.div<{ active: boolean, home?: boolean }>`
-    height: 58px;
-    display: flex;
-    align-items: center;
-    justify-content: flex-end;
-
-    > * {
-        // outline: 1px solid red;
-    }
-
-    > div {
-        width: 46px;
-        height: 46px;
-        display: grid;
-        place-items: center;
-        
-        border-start-start-radius: 50%;
-        border-end-start-radius: 50%;
-
-        &:active {
-            transform: translateY(1px);
-        }
-
-        ${ props => props.active && css`
-            background: var(--sidebar-active);
-            &:active {
-                transform: none;
-            }
-        ` }
-    }
-
-    span {
-        width: 6px;
-        height: 46px;
-
-        ${ props => props.active && css`
-            background-color: var(--sidebar-active);
-
-            &::before, &::after {
-                // outline: 1px solid blue;
-            }
-
-            &::before, &::after {
-                content: "";
-                display: block;
-                position: relative;
-
-                width: 31px;
-                height: 72px;
-                margin-top: -72px;
-                margin-left: -25px;
-                z-index: -1;
-
-                background-color: var(--background);
-                border-bottom-right-radius: 32px;
-
-                box-shadow: 0 32px 0 0 var(--sidebar-active);
-            }
-
-            &::after {
-                transform: scaleY(-1) translateY(-118px);
-            }
-        ` }
-    }
-
-    ${ props => (!props.active || props.home) && css`
-        cursor: pointer;
-    ` }
+const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
+	height: 58px;
+	display: flex;
+	align-items: center;
+	justify-content: flex-end;
+
+	> * {
+		// outline: 1px solid red;
+	}
+
+	> div {
+		width: 46px;
+		height: 46px;
+		display: grid;
+		place-items: center;
+
+		border-start-start-radius: 50%;
+		border-end-start-radius: 50%;
+
+		&:active {
+			transform: translateY(1px);
+		}
+
+		${(props) =>
+			props.active &&
+			css`
+				background: var(--sidebar-active);
+				&:active {
+					transform: none;
+				}
+			`}
+	}
+
+	span {
+		width: 6px;
+		height: 46px;
+
+		${(props) =>
+			props.active &&
+			css`
+				background-color: var(--sidebar-active);
+
+				&::before,
+				&::after {
+					// outline: 1px solid blue;
+				}
+
+				&::before,
+				&::after {
+					content: "";
+					display: block;
+					position: relative;
+
+					width: 31px;
+					height: 72px;
+					margin-top: -72px;
+					margin-left: -25px;
+					z-index: -1;
+
+					background-color: var(--background);
+					border-bottom-right-radius: 32px;
+
+					box-shadow: 0 32px 0 0 var(--sidebar-active);
+				}
+
+				&::after {
+					transform: scaleY(-1) translateY(-118px);
+				}
+			`}
+	}
+
+	${(props) =>
+		(!props.active || props.home) &&
+		css`
+			cursor: pointer;
+		`}
 `;
 
 interface Props {
-    unreads: Unreads;
-    lastOpened: LastOpened;
+	unreads: Unreads;
+	lastOpened: LastOpened;
 }
 
 export function ServerListSidebar({ unreads, lastOpened }: Props) {
-    const ctx = useForceUpdate();
-    const self = useSelf(ctx);
-    const activeServers = useServers(undefined, ctx) as Servers.Server[];
-    const channels = (useChannels(undefined, ctx) as Channel[])
-        .map(x => mapChannelWithUnread(x, unreads));
-    
-    const unreadChannels = channels.filter(x => x.unread)
-        .map(x => x._id);
-
-    const servers = activeServers.map(server => {
-        let alertCount = 0;
-        for (let id of server.channels) {
-            let channel = channels.find(x => x._id === id);
-            if (channel?.alertCount) {
-                alertCount += channel.alertCount;
-            }
-        }
-
-        return {
-            ...server,
-            unread: (typeof server.channels.find(x => unreadChannels.includes(x)) !== 'undefined' ?
-                ( alertCount > 0 ? 'mention' : 'unread' ) : undefined) as 'mention' | 'unread' | undefined,
-            alertCount
-        }
-    });
-
-    const path = useLocation().pathname;
-    const { server: server_id } = useParams<{ server?: string }>();
-    const server = servers.find(x => x!._id == server_id);
-
-    const { openScreen } = useIntermediate();
-
-    let homeUnread: 'mention' | 'unread' | undefined;
-    let alertCount = 0;
-    for (let x of channels) {
-        if (((x.channel_type === 'DirectMessage' && x.active) || x.channel_type === 'Group') && x.unread) {
-            homeUnread = 'unread';
-            alertCount += x.alertCount ?? 0;
-        }
-    }
-
-    if (alertCount > 0) homeUnread = 'mention';
-    const homeActive = typeof server === 'undefined' && !path.startsWith('/invite');
-
-    return (
-        <ServersBase>
-            <ServerList>
-                <ConditionalLink active={homeActive} to={lastOpened.home ? `/channel/${lastOpened.home}` : '/'}>
-                    <ServerEntry home active={homeActive}>
-                        <div onContextMenu={attachContextMenu('Status')}
-                            onClick={() => homeActive && openContextMenu("Status")}>
-                            <Icon size={42} unread={homeUnread}>
-                                <UserIcon target={self} size={32} status />
-                            </Icon>
-                        </div>
-                        <span />
-                    </ServerEntry>
-                </ConditionalLink>
-                <LineDivider />
-                {
-                    servers.map(entry => {
-                        const active = entry!._id === server?._id;
-                        const id = lastOpened[entry!._id];
-                        
-                        return (
-                            <ConditionalLink active={active} to={`/server/${entry!._id}` + (id ? `/channel/${id}` : '')}>
-                                <ServerEntry
-                                    active={active}
-                                    onContextMenu={attachContextMenu('Menu', { server: entry!._id })}>
-                                    <Tooltip content={entry.name} placement="right">
-                                        <Icon size={42} unread={entry.unread}>
-                                            <ServerIcon size={32} target={entry} />
-                                        </Icon>
-                                    </Tooltip>
-                                    <span />
-                                </ServerEntry>
-                            </ConditionalLink>
-                        )
-                    })
-                }
-                <IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_server' })}>
-                    <Plus size={36} />
-                </IconButton>
-                <PaintCounter small />
-            </ServerList>
-        </ServersBase>
-    )
+	const ctx = useForceUpdate();
+	const self = useSelf(ctx);
+	const activeServers = useServers(undefined, ctx) as Servers.Server[];
+	const channels = (useChannels(undefined, ctx) as Channel[]).map((x) =>
+		mapChannelWithUnread(x, unreads),
+	);
+
+	const unreadChannels = channels.filter((x) => x.unread).map((x) => x._id);
+
+	const servers = activeServers.map((server) => {
+		let alertCount = 0;
+		for (let id of server.channels) {
+			let channel = channels.find((x) => x._id === id);
+			if (channel?.alertCount) {
+				alertCount += channel.alertCount;
+			}
+		}
+
+		return {
+			...server,
+			unread: (typeof server.channels.find((x) =>
+				unreadChannels.includes(x),
+			) !== "undefined"
+				? alertCount > 0
+					? "mention"
+					: "unread"
+				: undefined) as "mention" | "unread" | undefined,
+			alertCount,
+		};
+	});
+
+	const path = useLocation().pathname;
+	const { server: server_id } = useParams<{ server?: string }>();
+	const server = servers.find((x) => x!._id == server_id);
+
+	const { openScreen } = useIntermediate();
+
+	let homeUnread: "mention" | "unread" | undefined;
+	let alertCount = 0;
+	for (let x of channels) {
+		if (
+			((x.channel_type === "DirectMessage" && x.active) ||
+				x.channel_type === "Group") &&
+			x.unread
+		) {
+			homeUnread = "unread";
+			alertCount += x.alertCount ?? 0;
+		}
+	}
+
+	if (alertCount > 0) homeUnread = "mention";
+	const homeActive =
+		typeof server === "undefined" && !path.startsWith("/invite");
+
+	return (
+		<ServersBase>
+			<ServerList>
+				<ConditionalLink
+					active={homeActive}
+					to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
+					<ServerEntry home active={homeActive}>
+						<div
+							onContextMenu={attachContextMenu("Status")}
+							onClick={() =>
+								homeActive && openContextMenu("Status")
+							}>
+							<Icon size={42} unread={homeUnread}>
+								<UserIcon target={self} size={32} status />
+							</Icon>
+						</div>
+						<span />
+					</ServerEntry>
+				</ConditionalLink>
+				<LineDivider />
+				{servers.map((entry) => {
+					const active = entry!._id === server?._id;
+					const id = lastOpened[entry!._id];
+
+					return (
+						<ConditionalLink
+							active={active}
+							to={
+								`/server/${entry!._id}` +
+								(id ? `/channel/${id}` : "")
+							}>
+							<ServerEntry
+								active={active}
+								onContextMenu={attachContextMenu("Menu", {
+									server: entry!._id,
+								})}>
+								<Tooltip content={entry.name} placement="right">
+									<Icon size={42} unread={entry.unread}>
+										<ServerIcon size={32} target={entry} />
+									</Icon>
+								</Tooltip>
+								<span />
+							</ServerEntry>
+						</ConditionalLink>
+					);
+				})}
+				<IconButton
+					onClick={() =>
+						openScreen({
+							id: "special_input",
+							type: "create_server",
+						})
+					}>
+					<Plus size={36} />
+				</IconButton>
+				<PaintCounter small />
+			</ServerList>
+		</ServersBase>
+	);
 }
 
-export default connectState(
-    ServerListSidebar,
-    state => {
-        return {
-            unreads: state.unreads,
-            lastOpened: state.lastOpened
-        };
-    }
-);
+export default connectState(ServerListSidebar, (state) => {
+	return {
+		unreads: state.unreads,
+		lastOpened: state.lastOpened,
+	};
+});
diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx
index 280dd8295c9a89677a58fde2c5740367d157d35f..38bc1645c96b204feb51e3912f35438b8a24442f 100644
--- a/src/components/navigation/left/ServerSidebar.tsx
+++ b/src/components/navigation/left/ServerSidebar.tsx
@@ -1,134 +1,150 @@
 import { Redirect, useParams } from "react-router";
-import { ChannelButton } from "../items/ButtonItem";
 import { Channels } from "revolt.js/dist/api/objects";
-import { Unreads } from "../../../redux/reducers/unreads";
-import { useChannels, useForceUpdate, useServer } from "../../../context/revoltjs/hooks";
-import { mapChannelWithUnread, useUnreads } from "./common";
-import ConnectionStatus from '../items/ConnectionStatus';
-import { connectState } from "../../../redux/connector";
-import PaintCounter from "../../../lib/PaintCounter";
 import styled from "styled-components";
-import { attachContextMenu } from 'preact-context-menu';
-import ServerHeader from "../../common/ServerHeader";
+
+import { attachContextMenu } from "preact-context-menu";
 import { useEffect } from "preact/hooks";
-import Category from "../../ui/Category";
-import { dispatch } from "../../../redux";
+
 import ConditionalLink from "../../../lib/ConditionalLink";
+import PaintCounter from "../../../lib/PaintCounter";
+
+import { dispatch } from "../../../redux";
+import { connectState } from "../../../redux/connector";
+import { Unreads } from "../../../redux/reducers/unreads";
+
+import {
+	useChannels,
+	useForceUpdate,
+	useServer,
+} from "../../../context/revoltjs/hooks";
+
 import CollapsibleSection from "../../common/CollapsibleSection";
+import ServerHeader from "../../common/ServerHeader";
+import Category from "../../ui/Category";
+import { mapChannelWithUnread, useUnreads } from "./common";
+
+import { ChannelButton } from "../items/ButtonItem";
+import ConnectionStatus from "../items/ConnectionStatus";
 
 interface Props {
-    unreads: Unreads;
+	unreads: Unreads;
 }
 
 const ServerBase = styled.div`
-    height: 100%;
-    width: 240px;
-    display: flex;
-    flex-shrink: 0;
-    flex-direction: column;
-    background: var(--secondary-background);
-
-    border-start-start-radius: 8px;
-    border-end-start-radius: 8px;
-    overflow: hidden;
+	height: 100%;
+	width: 240px;
+	display: flex;
+	flex-shrink: 0;
+	flex-direction: column;
+	background: var(--secondary-background);
+
+	border-start-start-radius: 8px;
+	border-end-start-radius: 8px;
+	overflow: hidden;
 `;
 
 const ServerList = styled.div`
-    padding: 6px;
-    flex-grow: 1;
-    overflow-y: scroll;
+	padding: 6px;
+	flex-grow: 1;
+	overflow-y: scroll;
 
-    > svg {
-        width: 100%;
-    }
+	> svg {
+		width: 100%;
+	}
 `;
 
 function ServerSidebar(props: Props) {
-    const { server: server_id, channel: channel_id } = useParams<{ server?: string, channel?: string }>();
-    const ctx = useForceUpdate();
-
-    const server = useServer(server_id, ctx);
-    if (!server) return <Redirect to="/" />;
-
-    const channels = (useChannels(server.channels, ctx)
-        .filter(entry => typeof entry !== 'undefined') as Readonly<Channels.TextChannel | Channels.VoiceChannel>[])
-        .map(x => mapChannelWithUnread(x, props.unreads));
-    
-    const channel = channels.find(x => x?._id === channel_id);
-    if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
-    if (channel) useUnreads({ ...props, channel }, ctx);
-
-    useEffect(() => {
-        if (!channel_id) return;
-
-        dispatch({
-            type: 'LAST_OPENED_SET',
-            parent: server_id!,
-            child: channel_id!
-        });
-    }, [ channel_id ]);
-
-    let uncategorised = new Set(server.channels);
-    let elements = [];
-    
-    function addChannel(id: string) {
-        const entry = channels.find(x => x._id === id);
-        if (!entry) return;
-
-        const active = channel?._id === entry._id;
-
-        return (
-            <ConditionalLink key={entry._id} active={active} to={`/server/${server!._id}/channel/${entry._id}`}>
-                <ChannelButton
-                    channel={entry}
-                    active={active}
-                    alert={entry.unread}
-                    compact
-                />
-            </ConditionalLink>
-        );
-    }
-
-    if (server.categories) {
-        for (let category of server.categories) {
-            let channels = [];
-            for (let id of category.channels) {
-                uncategorised.delete(id);
-                channels.push(addChannel(id));
-            }
-
-            elements.push(
-                <CollapsibleSection
-                    id={`category_${category.id}`}
-                    defaultValue
-                    summary={<Category text={category.title} />}>
-                    { channels }
-                </CollapsibleSection>
-            );
-        }
-    }
-
-    for (let id of Array.from(uncategorised).reverse()) {
-        elements.unshift(addChannel(id));
-    }
-
-    return (
-        <ServerBase>
-            <ServerHeader server={server} ctx={ctx} />
-            <ConnectionStatus />
-            <ServerList onContextMenu={attachContextMenu('Menu', { server_list: server._id })}>
-                { elements }
-            </ServerList>
-            <PaintCounter small />
-        </ServerBase>
-    )
-};
-
-export default connectState(
-    ServerSidebar,
-    state => {
-        return {
-            unreads: state.unreads
-        };
-    }
-);
+	const { server: server_id, channel: channel_id } =
+		useParams<{ server?: string; channel?: string }>();
+	const ctx = useForceUpdate();
+
+	const server = useServer(server_id, ctx);
+	if (!server) return <Redirect to="/" />;
+
+	const channels = (
+		useChannels(server.channels, ctx).filter(
+			(entry) => typeof entry !== "undefined",
+		) as Readonly<Channels.TextChannel | Channels.VoiceChannel>[]
+	).map((x) => mapChannelWithUnread(x, props.unreads));
+
+	const channel = channels.find((x) => x?._id === channel_id);
+	if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
+	if (channel) useUnreads({ ...props, channel }, ctx);
+
+	useEffect(() => {
+		if (!channel_id) return;
+
+		dispatch({
+			type: "LAST_OPENED_SET",
+			parent: server_id!,
+			child: channel_id!,
+		});
+	}, [channel_id]);
+
+	let uncategorised = new Set(server.channels);
+	let elements = [];
+
+	function addChannel(id: string) {
+		const entry = channels.find((x) => x._id === id);
+		if (!entry) return;
+
+		const active = channel?._id === entry._id;
+
+		return (
+			<ConditionalLink
+				key={entry._id}
+				active={active}
+				to={`/server/${server!._id}/channel/${entry._id}`}>
+				<ChannelButton
+					channel={entry}
+					active={active}
+					alert={entry.unread}
+					compact
+				/>
+			</ConditionalLink>
+		);
+	}
+
+	if (server.categories) {
+		for (let category of server.categories) {
+			let channels = [];
+			for (let id of category.channels) {
+				uncategorised.delete(id);
+				channels.push(addChannel(id));
+			}
+
+			elements.push(
+				<CollapsibleSection
+					id={`category_${category.id}`}
+					defaultValue
+					summary={<Category text={category.title} />}>
+					{channels}
+				</CollapsibleSection>,
+			);
+		}
+	}
+
+	for (let id of Array.from(uncategorised).reverse()) {
+		elements.unshift(addChannel(id));
+	}
+
+	return (
+		<ServerBase>
+			<ServerHeader server={server} ctx={ctx} />
+			<ConnectionStatus />
+			<ServerList
+				onContextMenu={attachContextMenu("Menu", {
+					server_list: server._id,
+				})}>
+				{elements}
+			</ServerList>
+			<PaintCounter small />
+		</ServerBase>
+	);
+}
+
+export default connectState(ServerSidebar, (state) => {
+	return {
+		unreads: state.unreads,
+	};
+});
diff --git a/src/components/navigation/left/common.ts b/src/components/navigation/left/common.ts
index f7ca164fbc47e1f2489952098294ec05366d894c..89168031ff38f0c853913e73b9e7f23877492bde 100644
--- a/src/components/navigation/left/common.ts
+++ b/src/components/navigation/left/common.ts
@@ -1,76 +1,103 @@
 import { Channel } from "revolt.js";
-import { dispatch } from "../../../redux";
+
 import { useLayoutEffect } from "preact/hooks";
+
+import { dispatch } from "../../../redux";
 import { Unreads } from "../../../redux/reducers/unreads";
+
 import { HookContext, useForceUpdate } from "../../../context/revoltjs/hooks";
 
 type UnreadProps = {
-    channel: Channel;
-    unreads: Unreads;
-}
+	channel: Channel;
+	unreads: Unreads;
+};
+
+export function useUnreads(
+	{ channel, unreads }: UnreadProps,
+	context?: HookContext,
+) {
+	const ctx = useForceUpdate(context);
 
-export function useUnreads({ channel, unreads }: UnreadProps, context?: HookContext) {
-    const ctx = useForceUpdate(context);
+	useLayoutEffect(() => {
+		function checkUnread(target?: Channel) {
+			if (!target) return;
+			if (target._id !== channel._id) return;
+			if (
+				target.channel_type === "SavedMessages" ||
+				target.channel_type === "VoiceChannel"
+			)
+				return;
 
-    useLayoutEffect(() => {
-        function checkUnread(target?: Channel) {
-            if (!target) return;
-            if (target._id !== channel._id) return;
-            if (target.channel_type === "SavedMessages" ||
-                target.channel_type === "VoiceChannel") return;
+			const unread = unreads[channel._id]?.last_id;
+			if (target.last_message) {
+				const message =
+					typeof target.last_message === "string"
+						? target.last_message
+						: target.last_message._id;
+				if (!unread || (unread && message.localeCompare(unread) > 0)) {
+					dispatch({
+						type: "UNREADS_MARK_READ",
+						channel: channel._id,
+						message,
+					});
 
-            const unread = unreads[channel._id]?.last_id;
-            if (target.last_message) {
-                const message = typeof target.last_message === 'string' ? target.last_message : target.last_message._id;
-                if (!unread || (unread && message.localeCompare(unread) > 0)) {
-                    dispatch({
-                        type: "UNREADS_MARK_READ",
-                        channel: channel._id,
-                        message
-                    });
-                    
-                    ctx.client.req('PUT', `/channels/${channel._id}/ack/${message}` as '/channels/id/ack/id');
-                }
-            }
-        }
+					ctx.client.req(
+						"PUT",
+						`/channels/${channel._id}/ack/${message}` as "/channels/id/ack/id",
+					);
+				}
+			}
+		}
 
-        checkUnread(channel);
+		checkUnread(channel);
 
-        ctx.client.channels.addListener("mutation", checkUnread);
-        return () => ctx.client.channels.removeListener("mutation", checkUnread);
-    }, [channel, unreads]);
+		ctx.client.channels.addListener("mutation", checkUnread);
+		return () =>
+			ctx.client.channels.removeListener("mutation", checkUnread);
+	}, [channel, unreads]);
 }
 
 export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
-    let last_message_id;
-    if (channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group') {
-        last_message_id = channel.last_message?._id;
-    } else if (channel.channel_type === 'TextChannel') {
-        last_message_id = channel.last_message;
-    } else {
-        return { ...channel, unread: undefined, alertCount: undefined, timestamp: channel._id };
-    }
+	let last_message_id;
+	if (
+		channel.channel_type === "DirectMessage" ||
+		channel.channel_type === "Group"
+	) {
+		last_message_id = channel.last_message?._id;
+	} else if (channel.channel_type === "TextChannel") {
+		last_message_id = channel.last_message;
+	} else {
+		return {
+			...channel,
+			unread: undefined,
+			alertCount: undefined,
+			timestamp: channel._id,
+		};
+	}
 
-    let unread: 'mention' | 'unread' | undefined;
-    let alertCount: undefined | number;
-    if (last_message_id && unreads) {
-        const u = unreads[channel._id];
-        if (u) {
-            if (u.mentions && u.mentions.length > 0) {
-                alertCount = u.mentions.length;
-                unread = 'mention';
-            } else if (u.last_id && last_message_id.localeCompare(u.last_id) > 0) {
-                unread = 'unread';
-            }
-        } else {
-            unread = 'unread';
-        }
-    }
+	let unread: "mention" | "unread" | undefined;
+	let alertCount: undefined | number;
+	if (last_message_id && unreads) {
+		const u = unreads[channel._id];
+		if (u) {
+			if (u.mentions && u.mentions.length > 0) {
+				alertCount = u.mentions.length;
+				unread = "mention";
+			} else if (
+				u.last_id &&
+				last_message_id.localeCompare(u.last_id) > 0
+			) {
+				unread = "unread";
+			}
+		} else {
+			unread = "unread";
+		}
+	}
 
-    return {
-        ...channel,
-        timestamp: last_message_id ?? channel._id,
-        unread,
-        alertCount
-    };
+	return {
+		...channel,
+		timestamp: last_message_id ?? channel._id,
+		unread,
+		alertCount,
+	};
 }
diff --git a/src/components/navigation/right/ChannelDebugInfo.tsx b/src/components/navigation/right/ChannelDebugInfo.tsx
index 8584288176818f11a8eb58ea2634eaceb6f4650b..392f547ee13efec324ff5b3f7e37c4caee74d1db 100644
--- a/src/components/navigation/right/ChannelDebugInfo.tsx
+++ b/src/components/navigation/right/ChannelDebugInfo.tsx
@@ -1,37 +1,40 @@
 import { useRenderState } from "../../../lib/renderer/Singleton";
 
 interface Props {
-    id: string;
+	id: string;
 }
 
 export function ChannelDebugInfo({ id }: Props) {
-    if (process.env.NODE_ENV !== "development") return null;
-    let view = useRenderState(id);
-    if (!view) return null;
+	if (process.env.NODE_ENV !== "development") return null;
+	let view = useRenderState(id);
+	if (!view) return null;
 
-    return (
-        <span style={{ display: "block", padding: "12px 10px 0 10px" }}>
-            <span
-                style={{
-                    display: "block",
-                    fontSize: "12px",
-                    textTransform: "uppercase",
-                    fontWeight: "600"
-                }}
-            >
-                Channel Info
-            </span>
-            <p style={{ fontSize: "10px", userSelect: "text" }}>
-                State: <b>{ view.type }</b> <br />
-                { view.type === 'RENDER' && view.messages.length > 0 &&
-                    <>
-                        Start: <b>{view.messages[0]._id}</b> <br />
-                        End: <b>{view.messages[view.messages.length - 1]._id}</b> <br />
-                        At Top: <b>{view.atTop ? "Yes" : "No"}</b> <br />
-                        At Bottom: <b>{view.atBottom ? "Yes" : "No"}</b>
-                    </>
-                }
-            </p>
-        </span>
-    );
+	return (
+		<span style={{ display: "block", padding: "12px 10px 0 10px" }}>
+			<span
+				style={{
+					display: "block",
+					fontSize: "12px",
+					textTransform: "uppercase",
+					fontWeight: "600",
+				}}>
+				Channel Info
+			</span>
+			<p style={{ fontSize: "10px", userSelect: "text" }}>
+				State: <b>{view.type}</b> <br />
+				{view.type === "RENDER" && view.messages.length > 0 && (
+					<>
+						Start: <b>{view.messages[0]._id}</b> <br />
+						End:{" "}
+						<b>
+							{view.messages[view.messages.length - 1]._id}
+						</b>{" "}
+						<br />
+						At Top: <b>{view.atTop ? "Yes" : "No"}</b> <br />
+						At Bottom: <b>{view.atBottom ? "Yes" : "No"}</b>
+					</>
+				)}
+			</p>
+		</span>
+	);
 }
diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx
index 40be8041dd0fcae9d11a6313c9a0a680bdadff88..a24fc9379ec6acfb6af52a0252eb2a5ce6a2b1d7 100644
--- a/src/components/navigation/right/MemberSidebar.tsx
+++ b/src/components/navigation/right/MemberSidebar.tsx
@@ -1,45 +1,62 @@
+import { useParams } from "react-router";
+import { User } from "revolt.js";
+import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
+import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
+
 import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
 
-import { User } from "revolt.js";
-import Category from "../../ui/Category";
-import { useParams } from "react-router";
-import { UserButton } from "../items/ButtonItem";
-import { ChannelDebugInfo } from "./ChannelDebugInfo";
-import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
-import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
-import { AppContext, ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
-import { HookContext, useChannel, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
+import {
+	AppContext,
+	ClientStatus,
+	StatusContext,
+} from "../../../context/revoltjs/RevoltClient";
+import {
+	HookContext,
+	useChannel,
+	useForceUpdate,
+	useUsers,
+} from "../../../context/revoltjs/hooks";
 
-import placeholderSVG from "../items/placeholder.svg";
+import Category from "../../ui/Category";
 import Preloader from "../../ui/Preloader";
+import placeholderSVG from "../items/placeholder.svg";
+
+import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
+import { UserButton } from "../items/ButtonItem";
+import { ChannelDebugInfo } from "./ChannelDebugInfo";
 
 interface Props {
-    ctx: HookContext
+	ctx: HookContext;
 }
 
 export default function MemberSidebar(props: { channel?: Channels.Channel }) {
-    const ctx = useForceUpdate();
-    const { channel: cid } = useParams<{ channel: string }>();
-    const channel = props.channel ?? useChannel(cid, ctx);
-
-    switch (channel?.channel_type) {
-        case 'Group': return <GroupMemberSidebar channel={channel} ctx={ctx} />;
-        case 'TextChannel': return <ServerMemberSidebar channel={channel} ctx={ctx} />;
-        default: return null;
-    }
+	const ctx = useForceUpdate();
+	const { channel: cid } = useParams<{ channel: string }>();
+	const channel = props.channel ?? useChannel(cid, ctx);
+
+	switch (channel?.channel_type) {
+		case "Group":
+			return <GroupMemberSidebar channel={channel} ctx={ctx} />;
+		case "TextChannel":
+			return <ServerMemberSidebar channel={channel} ctx={ctx} />;
+		default:
+			return null;
+	}
 }
 
-export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels.GroupChannel }) {
-    const { openScreen } = useIntermediate();
-    const users = useUsers(undefined, ctx);
-    let members = channel.recipients
-        .map(x => users.find(y => y?._id === x))
-        .filter(x => typeof x !== "undefined") as User[];
+export function GroupMemberSidebar({
+	channel,
+	ctx,
+}: Props & { channel: Channels.GroupChannel }) {
+	const { openScreen } = useIntermediate();
+	const users = useUsers(undefined, ctx);
+	let members = channel.recipients
+		.map((x) => users.find((y) => y?._id === x))
+		.filter((x) => typeof x !== "undefined") as User[];
 
-    /*const voice = useContext(VoiceContext);
+	/*const voice = useContext(VoiceContext);
     const voiceActive = voice.roomId === channel._id;
 
     let voiceParticipants: User[] = [];
@@ -54,24 +71,32 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
         voiceParticipants.sort((a, b) => a.username.localeCompare(b.username));
     }*/
 
-    members.sort((a, b) => {
-        // ! FIXME: should probably rewrite all this code
-        let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
-        let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
+	members.sort((a, b) => {
+		// ! FIXME: should probably rewrite all this code
+		let l =
+			+(
+				(a.online && a.status?.presence !== Users.Presence.Invisible) ??
+				false
+			) | 0;
+		let r =
+			+(
+				(b.online && b.status?.presence !== Users.Presence.Invisible) ??
+				false
+			) | 0;
 
-        let n = r - l;
-        if (n !== 0) {
-            return n;
-        }
+		let n = r - l;
+		if (n !== 0) {
+			return n;
+		}
 
-        return a.username.localeCompare(b.username);
-    });
+		return a.username.localeCompare(b.username);
+	});
 
-    return (
-        <GenericSidebarBase>
-            <GenericSidebarList>
-                <ChannelDebugInfo id={channel._id} />
-                {/*voiceActive && voiceParticipants.length !== 0 && (
+	return (
+		<GenericSidebarBase>
+			<GenericSidebarList>
+				<ChannelDebugInfo id={channel._id} />
+				{/*voiceActive && voiceParticipants.length !== 0 && (
                     <Fragment>
                         <Category
                             type="members"
@@ -96,104 +121,146 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
                         )}
                     </Fragment>
                 )*/}
-                {!(members.length === 0 /*&& voiceActive*/) && (
-                    <Category
-                        variant="uniform"
-                        text={
-                            <span>
-                                <Text id="app.main.categories.members" /> —{" "}
-                                {channel.recipients.length}
-                            </span>
-                        }
-                    />
-                )}
-                {members.length === 0 && /*!voiceActive &&*/ <img src={placeholderSVG} />}
-                {members.map(
-                    user =>
-                        user && (
-                            <UserButton
-                                key={user._id}
-                                user={user}
-                                context={channel}
-                                onClick={() => openScreen({ id: 'profile', user_id: user._id })} />
-                        )
-                )}
-            </GenericSidebarList>
-        </GenericSidebarBase>
-    );
+				{!((members.length === 0) /*&& voiceActive*/) && (
+					<Category
+						variant="uniform"
+						text={
+							<span>
+								<Text id="app.main.categories.members" /> —{" "}
+								{channel.recipients.length}
+							</span>
+						}
+					/>
+				)}
+				{members.length === 0 && (
+					/*!voiceActive &&*/ <img src={placeholderSVG} />
+				)}
+				{members.map(
+					(user) =>
+						user && (
+							<UserButton
+								key={user._id}
+								user={user}
+								context={channel}
+								onClick={() =>
+									openScreen({
+										id: "profile",
+										user_id: user._id,
+									})
+								}
+							/>
+						),
+				)}
+			</GenericSidebarList>
+		</GenericSidebarBase>
+	);
 }
 
-export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channels.TextChannel }) {
-    const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined);
-    const users = useUsers(members?.map(x => x._id.user) ?? []).filter(x => typeof x !== 'undefined', ctx) as Users.User[];
-    const { openScreen } = useIntermediate();
-    const status = useContext(StatusContext);
-    const client = useContext(AppContext);
-
-    useEffect(() => {
-        if (status === ClientStatus.ONLINE && typeof members === 'undefined') {
-            client.servers.members.fetchMembers(channel.server)
-                .then(members => setMembers(members))
-        }
-    }, [ status ]);
-
-    // ! FIXME: temporary code
-    useEffect(() => {
-        function onPacket(packet: ClientboundNotification) {
-            if (!members) return;
-            if (packet.type === 'ServerMemberJoin') {
-                if (packet.id !== channel.server) return;
-                setMembers([ ...members, { _id: { server: packet.id, user: packet.user } } ]);
-            } else if (packet.type === 'ServerMemberLeave') {
-                if (packet.id !== channel.server) return;
-                setMembers(members.filter(x => !(x._id.user === packet.user && x._id.server === packet.id)));
-            }
-        }
-
-        client.addListener('packet', onPacket);
-        return () => client.removeListener('packet', onPacket);
-    }, [ members ]);
-
-    // copy paste from above
-    users.sort((a, b) => {
-        // ! FIXME: should probably rewrite all this code
-        let l = +((a.online && a.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
-        let r = +((b.online && b.status?.presence !== Users.Presence.Invisible) ?? false) | 0;
-
-        let n = r - l;
-        if (n !== 0) {
-            return n;
-        }
-
-        return a.username.localeCompare(b.username);
-    });
-
-    return (
-        <GenericSidebarBase>
-            <GenericSidebarList>
-                <ChannelDebugInfo id={channel._id} />
-                <Category
-                    variant="uniform"
-                    text={
-                        <span>
-                            <Text id="app.main.categories.members" /> —{" "}
-                            {users.length}
-                        </span>
-                    }
-                />
-                {!members && <Preloader type="ring" />}
-                {members && users.length === 0 && <img src={placeholderSVG} />}
-                {users.map(
-                    user =>
-                        user && (
-                            <UserButton
-                                key={user._id}
-                                user={user}
-                                context={channel}
-                                onClick={() => openScreen({ id: 'profile', user_id: user._id })} />
-                        )
-                )}
-            </GenericSidebarList>
-        </GenericSidebarBase>
-    );
+export function ServerMemberSidebar({
+	channel,
+	ctx,
+}: Props & { channel: Channels.TextChannel }) {
+	const [members, setMembers] = useState<Servers.Member[] | undefined>(
+		undefined,
+	);
+	const users = useUsers(members?.map((x) => x._id.user) ?? []).filter(
+		(x) => typeof x !== "undefined",
+		ctx,
+	) as Users.User[];
+	const { openScreen } = useIntermediate();
+	const status = useContext(StatusContext);
+	const client = useContext(AppContext);
+
+	useEffect(() => {
+		if (status === ClientStatus.ONLINE && typeof members === "undefined") {
+			client.servers.members
+				.fetchMembers(channel.server)
+				.then((members) => setMembers(members));
+		}
+	}, [status]);
+
+	// ! FIXME: temporary code
+	useEffect(() => {
+		function onPacket(packet: ClientboundNotification) {
+			if (!members) return;
+			if (packet.type === "ServerMemberJoin") {
+				if (packet.id !== channel.server) return;
+				setMembers([
+					...members,
+					{ _id: { server: packet.id, user: packet.user } },
+				]);
+			} else if (packet.type === "ServerMemberLeave") {
+				if (packet.id !== channel.server) return;
+				setMembers(
+					members.filter(
+						(x) =>
+							!(
+								x._id.user === packet.user &&
+								x._id.server === packet.id
+							),
+					),
+				);
+			}
+		}
+
+		client.addListener("packet", onPacket);
+		return () => client.removeListener("packet", onPacket);
+	}, [members]);
+
+	// copy paste from above
+	users.sort((a, b) => {
+		// ! FIXME: should probably rewrite all this code
+		let l =
+			+(
+				(a.online && a.status?.presence !== Users.Presence.Invisible) ??
+				false
+			) | 0;
+		let r =
+			+(
+				(b.online && b.status?.presence !== Users.Presence.Invisible) ??
+				false
+			) | 0;
+
+		let n = r - l;
+		if (n !== 0) {
+			return n;
+		}
+
+		return a.username.localeCompare(b.username);
+	});
+
+	return (
+		<GenericSidebarBase>
+			<GenericSidebarList>
+				<ChannelDebugInfo id={channel._id} />
+				<Category
+					variant="uniform"
+					text={
+						<span>
+							<Text id="app.main.categories.members" /> —{" "}
+							{users.length}
+						</span>
+					}
+				/>
+				{!members && <Preloader type="ring" />}
+				{members && users.length === 0 && <img src={placeholderSVG} />}
+				{users.map(
+					(user) =>
+						user && (
+							<UserButton
+								key={user._id}
+								user={user}
+								context={channel}
+								onClick={() =>
+									openScreen({
+										id: "profile",
+										user_id: user._id,
+									})
+								}
+							/>
+						),
+				)}
+			</GenericSidebarList>
+		</GenericSidebarBase>
+	);
 }
diff --git a/src/components/ui/Banner.tsx b/src/components/ui/Banner.tsx
index 4b96d20529087d7b5645239279575ad6f27fd83f..1ff99f8344f3127afa1c83bb217f7b3d4ba37565 100644
--- a/src/components/ui/Banner.tsx
+++ b/src/components/ui/Banner.tsx
@@ -1,10 +1,10 @@
 import styled from "styled-components";
 
 export default styled.div`
-    padding: 8px;
-    font-size: 14px;
-    text-align: center;
+	padding: 8px;
+	font-size: 14px;
+	text-align: center;
 
-    color: var(--accent);
-    background: var(--primary-background);
+	color: var(--accent);
+	background: var(--primary-background);
 `;
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 870e7ed22a463a87be437f63f97039f21925c89d..7604da39a277e754ca885b2f93edb89075dfab2a 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -1,71 +1,71 @@
 import styled, { css } from "styled-components";
 
 interface Props {
-    readonly contrast?: boolean;
-    readonly error?: boolean;
+	readonly contrast?: boolean;
+	readonly error?: boolean;
 }
 
 export default styled.button<Props>`
-    z-index: 1;
-    padding: 8px;
-    font-size: 16px;
-    text-align: center;
-    font-family: inherit;
+	z-index: 1;
+	padding: 8px;
+	font-size: 16px;
+	text-align: center;
+	font-family: inherit;
 
-    transition: 0.2s ease opacity;
-    transition: 0.2s ease background-color;
+	transition: 0.2s ease opacity;
+	transition: 0.2s ease background-color;
 
-    background: var(--primary-background);
-    color: var(--foreground);
+	background: var(--primary-background);
+	color: var(--foreground);
 
-    border-radius: 6px;
-    cursor: pointer;
-    border: none;
+	border-radius: 6px;
+	cursor: pointer;
+	border: none;
 
-    &:hover {
-        background: var(--secondary-header);
-    }
+	&:hover {
+		background: var(--secondary-header);
+	}
 
-    &:disabled {
-        background: var(--primary-background);
-    }
+	&:disabled {
+		background: var(--primary-background);
+	}
 
-    &:active {
-        background: var(--secondary-background);
-    }
+	&:active {
+		background: var(--secondary-background);
+	}
 
-    ${(props) =>
-        props.contrast &&
-        css`
-            padding: 4px 8px;
-            background: var(--secondary-header);
+	${(props) =>
+		props.contrast &&
+		css`
+			padding: 4px 8px;
+			background: var(--secondary-header);
 
-            &:hover {
-                background: var(--primary-header);
-            }
+			&:hover {
+				background: var(--primary-header);
+			}
 
-            &:disabled {
-                background: var(--secondary-header);
-            }
+			&:disabled {
+				background: var(--secondary-header);
+			}
 
-            &:active {
-                background: var(--secondary-background);
-            }
-        `}
+			&:active {
+				background: var(--secondary-background);
+			}
+		`}
 
-    ${(props) =>
-        props.error &&
-        css`
-            color: white;
-            background: var(--error);
+	${(props) =>
+		props.error &&
+		css`
+			color: white;
+			background: var(--error);
 
-            &:hover {
-                filter: brightness(1.2);
-                background: var(--error);
-            }
+			&:hover {
+				filter: brightness(1.2);
+				background: var(--error);
+			}
 
-            &:disabled {
-                background: var(--error);
-            }
-        `}
+			&:disabled {
+				background: var(--error);
+			}
+		`}
 `;
diff --git a/src/components/ui/Category.tsx b/src/components/ui/Category.tsx
index 3f76d2846d585b5b5beaabdfe5b3e5d8e793b020..80d117cfa1fcb45a4514faaafccd38fcda958af8 100644
--- a/src/components/ui/Category.tsx
+++ b/src/components/ui/Category.tsx
@@ -1,51 +1,55 @@
+import { Plus } from "@styled-icons/boxicons-regular";
 import styled, { css } from "styled-components";
+
 import { Children } from "../../types/Preact";
-import { Plus } from "@styled-icons/boxicons-regular";
 
-const CategoryBase = styled.div<Pick<Props, 'variant'>>`
-    font-size: 12px;
-    font-weight: 700;
-    text-transform: uppercase;
-
-    margin-top: 4px;
-    padding: 6px 0;
-    margin-bottom: 4px;
-    white-space: nowrap;
-    
-    display: flex;
-    align-items: center;
-    flex-direction: row;
-    justify-content: space-between;
-
-    svg {
-        cursor: pointer;
-    }
-
-    &:first-child {
-        margin-top: 0;
-        padding-top: 0;
-    }
-
-    ${ props => props.variant === 'uniform' && css`
-        padding-top: 6px;
-    ` }
+const CategoryBase = styled.div<Pick<Props, "variant">>`
+	font-size: 12px;
+	font-weight: 700;
+	text-transform: uppercase;
+
+	margin-top: 4px;
+	padding: 6px 0;
+	margin-bottom: 4px;
+	white-space: nowrap;
+
+	display: flex;
+	align-items: center;
+	flex-direction: row;
+	justify-content: space-between;
+
+	svg {
+		cursor: pointer;
+	}
+
+	&:first-child {
+		margin-top: 0;
+		padding-top: 0;
+	}
+
+	${(props) =>
+		props.variant === "uniform" &&
+		css`
+			padding-top: 6px;
+		`}
 `;
 
-type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as' | 'action'> & {
-    text: Children;
-    action?: () => void;
-    variant?: 'default' | 'uniform';
-}
+type Props = Omit<
+	JSX.HTMLAttributes<HTMLDivElement>,
+	"children" | "as" | "action"
+> & {
+	text: Children;
+	action?: () => void;
+	variant?: "default" | "uniform";
+};
 
 export default function Category(props: Props) {
-    let { text, action, ...otherProps } = props;
-
-    return (
-        <CategoryBase {...otherProps}>
-            {text}
-            {action && (
-                <Plus size={16} onClick={action} />
-            )}
-        </CategoryBase>
-    );
-};
+	let { text, action, ...otherProps } = props;
+
+	return (
+		<CategoryBase {...otherProps}>
+			{text}
+			{action && <Plus size={16} onClick={action} />}
+		</CategoryBase>
+	);
+}
diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx
index 7fad94d8adccb67348effc20a8f7867804b797ab..44d80114fff465e580bc6ff43e0b79eafc890689 100644
--- a/src/components/ui/Checkbox.tsx
+++ b/src/components/ui/Checkbox.tsx
@@ -1,107 +1,108 @@
 import { Check } from "@styled-icons/boxicons-regular";
-import { Children } from "../../types/Preact";
 import styled, { css } from "styled-components";
 
+import { Children } from "../../types/Preact";
+
 const CheckboxBase = styled.label`
-    margin-top: 20px;
-    gap: 4px;
-    z-index: 1;
-    display: flex;
-    border-radius: 4px;
-    align-items: center;
+	margin-top: 20px;
+	gap: 4px;
+	z-index: 1;
+	display: flex;
+	border-radius: 4px;
+	align-items: center;
 
-    cursor: pointer;
-    font-size: 18px;
-    user-select: none;
+	cursor: pointer;
+	font-size: 18px;
+	user-select: none;
 
-    transition: 0.2s ease all;
+	transition: 0.2s ease all;
 
-    input {
-        display: none;
-    }
+	input {
+		display: none;
+	}
 
-    &:hover {
-        .check {
-            background: var(--background);
-        }
-    }
+	&:hover {
+		.check {
+			background: var(--background);
+		}
+	}
 
-    &[disabled] {
-        opacity: .5;
-        cursor: not-allowed;
+	&[disabled] {
+		opacity: 0.5;
+		cursor: not-allowed;
 
-        &:hover {
-            background: unset;
-        }
-    }
+		&:hover {
+			background: unset;
+		}
+	}
 `;
 
 const CheckboxContent = styled.span`
-    display: flex;
-    flex-grow: 1;
-    font-size: 1rem;
-    font-weight: 600;
-    flex-direction: column;
+	display: flex;
+	flex-grow: 1;
+	font-size: 1rem;
+	font-weight: 600;
+	flex-direction: column;
 `;
 
 const CheckboxDescription = styled.span`
-    font-size: .75rem;
-    font-weight: 400;
-    color: var(--secondary-foreground);
+	font-size: 0.75rem;
+	font-weight: 400;
+	color: var(--secondary-foreground);
 `;
 
 const Checkmark = styled.div<{ checked: boolean }>`
-    margin: 4px;
-    width: 24px;
-    height: 24px;
-    display: grid;
-    flex-shrink: 0;
-    border-radius: 4px;
-    place-items: center;
-    transition: 0.2s ease all;
-    background: var(--secondary-background);
+	margin: 4px;
+	width: 24px;
+	height: 24px;
+	display: grid;
+	flex-shrink: 0;
+	border-radius: 4px;
+	place-items: center;
+	transition: 0.2s ease all;
+	background: var(--secondary-background);
 
-    svg {
-        color: var(--secondary-background);
-    }
+	svg {
+		color: var(--secondary-background);
+	}
 
-    ${(props) =>
-        props.checked &&
-        css`
-            background: var(--accent) !important;
-        `}
+	${(props) =>
+		props.checked &&
+		css`
+			background: var(--accent) !important;
+		`}
 `;
 
 export interface CheckboxProps {
-    checked: boolean;
-    disabled?: boolean;
-    className?: string;
-    children: Children;
-    description?: Children;
-    onChange: (state: boolean) => void;
+	checked: boolean;
+	disabled?: boolean;
+	className?: string;
+	children: Children;
+	description?: Children;
+	onChange: (state: boolean) => void;
 }
 
 export default function Checkbox(props: CheckboxProps) {
-    return (
-        <CheckboxBase disabled={props.disabled} className={props.className}>
-            <CheckboxContent>
-                <span>{props.children}</span>
-                {props.description && (
-                    <CheckboxDescription>
-                        {props.description}
-                    </CheckboxDescription>
-                )}
-            </CheckboxContent>
-            <input
-                type="checkbox"
-                checked={props.checked}
-                onChange={() =>
-                    !props.disabled && props.onChange(!props.checked)
-                }
-            />
-            <Checkmark checked={props.checked} className="check">
-                <Check size={20} />
-            </Checkmark>
-        </CheckboxBase>
-    );
+	return (
+		<CheckboxBase disabled={props.disabled} className={props.className}>
+			<CheckboxContent>
+				<span>{props.children}</span>
+				{props.description && (
+					<CheckboxDescription>
+						{props.description}
+					</CheckboxDescription>
+				)}
+			</CheckboxContent>
+			<input
+				type="checkbox"
+				checked={props.checked}
+				onChange={() =>
+					!props.disabled && props.onChange(!props.checked)
+				}
+			/>
+			<Checkmark checked={props.checked} className="check">
+				<Check size={20} />
+			</Checkmark>
+		</CheckboxBase>
+	);
 }
diff --git a/src/components/ui/ColourSwatches.tsx b/src/components/ui/ColourSwatches.tsx
index 46a473fc80ca6077b82daf219088d95fad4b49b1..b0bd781ef1906a121f818a509e5258506a68455b 100644
--- a/src/components/ui/ColourSwatches.tsx
+++ b/src/components/ui/ColourSwatches.tsx
@@ -1,130 +1,127 @@
-import { useRef } from "preact/hooks";
 import { Check } from "@styled-icons/boxicons-regular";
 import { Palette } from "@styled-icons/boxicons-solid";
 import styled, { css } from "styled-components";
 
+import { useRef } from "preact/hooks";
+
 interface Props {
-    value: string;
-    onChange: (value: string) => void;
+	value: string;
+	onChange: (value: string) => void;
 }
 
 const presets = [
-    [
-        "#7B68EE",
-        "#3498DB",
-        "#1ABC9C",
-        "#F1C40F",
-        "#FF7F50",
-        "#FD6671",
-        "#E91E63",
-        "#D468EE",
-    ],
-    [
-        "#594CAD",
-        "#206694",
-        "#11806A",
-        "#C27C0E",
-        "#CD5B45",
-        "#FF424F",
-        "#AD1457",
-        "#954AA8",
-    ],
+	[
+		"#7B68EE",
+		"#3498DB",
+		"#1ABC9C",
+		"#F1C40F",
+		"#FF7F50",
+		"#FD6671",
+		"#E91E63",
+		"#D468EE",
+	],
+	[
+		"#594CAD",
+		"#206694",
+		"#11806A",
+		"#C27C0E",
+		"#CD5B45",
+		"#FF424F",
+		"#AD1457",
+		"#954AA8",
+	],
 ];
 
 const SwatchesBase = styled.div`
-    gap: 8px;
-    display: flex;
+	gap: 8px;
+	display: flex;
 
-    input {
-        opacity: 0;
-        margin-top: 44px;
-        position: absolute;
-        pointer-events: none;
-    }
+	input {
+		opacity: 0;
+		margin-top: 44px;
+		position: absolute;
+		pointer-events: none;
+	}
 `;
 
 const Swatch = styled.div<{ type: "small" | "large"; colour: string }>`
-    flex-shrink: 0;
-    cursor: pointer;
-    border-radius: 4px;
-    background-color: ${(props) => props.colour};
+	flex-shrink: 0;
+	cursor: pointer;
+	border-radius: 4px;
+	background-color: ${(props) => props.colour};
 
-    display: grid;
-    place-items: center;
+	display: grid;
+	place-items: center;
 
-    &:hover {
-        border: 3px solid var(--foreground);
-        transition: border ease-in-out 0.07s;
-    }
+	&:hover {
+		border: 3px solid var(--foreground);
+		transition: border ease-in-out 0.07s;
+	}
 
-    svg {
-        color: white;
-    }
+	svg {
+		color: white;
+	}
 
-    ${(props) =>
-        props.type === "small"
-            ? css`
-                  width: 30px;
-                  height: 30px;
+	${(props) =>
+		props.type === "small"
+			? css`
+					width: 30px;
+					height: 30px;
 
-                  svg {
-                      /*stroke-width: 2;*/
-                  }
-              `
-            : css`
-                  width: 68px;
-                  height: 68px;
-              `}
+					svg {
+						/*stroke-width: 2;*/
+					}
+			  `
+			: css`
+					width: 68px;
+					height: 68px;
+			  `}
 `;
 
 const Rows = styled.div`
-    gap: 8px;
-    display: flex;
-    flex-direction: column;
+	gap: 8px;
+	display: flex;
+	flex-direction: column;
 
-    > div {
-        gap: 8px;
-        display: flex;
-        flex-direction: row;
-    }
+	> div {
+		gap: 8px;
+		display: flex;
+		flex-direction: row;
+	}
 `;
 
 export default function ColourSwatches({ value, onChange }: Props) {
-    const ref = useRef<HTMLInputElement>();
+	const ref = useRef<HTMLInputElement>();
 
-    return (
-        <SwatchesBase>
-            <Swatch
-                colour={value}
-                type="large"
-                onClick={() => ref.current.click()}
-            >
-                <Palette size={32} />
-            </Swatch>
-            <input
-                type="color"
-                value={value}
-                ref={ref}
-                onChange={(ev) => onChange(ev.currentTarget.value)}
-            />
-            <Rows>
-                {presets.map((row, i) => (
-                    <div key={i}>
-                        {row.map((swatch, i) => (
-                            <Swatch
-                                colour={swatch}
-                                type="small"
-                                key={i}
-                                onClick={() => onChange(swatch)}
-                            >
-                                {swatch === value && (
-                                    <Check size={18} />
-                                )}
-                            </Swatch>
-                        ))}
-                    </div>
-                ))}
-            </Rows>
-        </SwatchesBase>
-    );
+	return (
+		<SwatchesBase>
+			<Swatch
+				colour={value}
+				type="large"
+				onClick={() => ref.current.click()}>
+				<Palette size={32} />
+			</Swatch>
+			<input
+				type="color"
+				value={value}
+				ref={ref}
+				onChange={(ev) => onChange(ev.currentTarget.value)}
+			/>
+			<Rows>
+				{presets.map((row, i) => (
+					<div key={i}>
+						{row.map((swatch, i) => (
+							<Swatch
+								colour={swatch}
+								type="small"
+								key={i}
+								onClick={() => onChange(swatch)}>
+								{swatch === value && <Check size={18} />}
+							</Swatch>
+						))}
+					</div>
+				))}
+			</Rows>
+		</SwatchesBase>
+	);
 }
diff --git a/src/components/ui/ComboBox.tsx b/src/components/ui/ComboBox.tsx
index 989f0787b351c5b9e8c632494956de7e14ade81f..55911247d22fa407fa201c665a4e1267fc64ecfa 100644
--- a/src/components/ui/ComboBox.tsx
+++ b/src/components/ui/ComboBox.tsx
@@ -1,20 +1,20 @@
 import styled from "styled-components";
 
 export default styled.select`
-    padding: 8px;
-    border-radius: 6px;
-    font-family: inherit;
-    color: var(--secondary-foreground);
-    background: var(--secondary-background);
-    font-size: .875rem;
-    border: none;
-    outline: 2px solid transparent;
-    transition: outline-color 0.2s ease-in-out;
-    transition: box-shadow .3s;
-    cursor: pointer;
-    width: 100%;
+	padding: 8px;
+	border-radius: 6px;
+	font-family: inherit;
+	color: var(--secondary-foreground);
+	background: var(--secondary-background);
+	font-size: 0.875rem;
+	border: none;
+	outline: 2px solid transparent;
+	transition: outline-color 0.2s ease-in-out;
+	transition: box-shadow 0.3s;
+	cursor: pointer;
+	width: 100%;
 
-    &:focus {
-        box-shadow: 0 0 0 2pt var(--accent);
-    }
+	&:focus {
+		box-shadow: 0 0 0 2pt var(--accent);
+	}
 `;
diff --git a/src/components/ui/DateDivider.tsx b/src/components/ui/DateDivider.tsx
index 5f6e1a0af05e73af8347794bc3b25923e31cce95..4f2a9dd5bca12b2f53eab4e5599e330926887055 100644
--- a/src/components/ui/DateDivider.tsx
+++ b/src/components/ui/DateDivider.tsx
@@ -2,47 +2,47 @@ import dayjs from "dayjs";
 import styled, { css } from "styled-components";
 
 const Base = styled.div<{ unread?: boolean }>`
-    height: 0;
-    display: flex;
-    user-select: none;
-    align-items: center;
-    margin: 17px 12px 5px;
-    border-top: thin solid var(--tertiary-foreground);
+	height: 0;
+	display: flex;
+	user-select: none;
+	align-items: center;
+	margin: 17px 12px 5px;
+	border-top: thin solid var(--tertiary-foreground);
 
-    time {
-        margin-top: -2px;
-        font-size: .6875rem;
-        line-height: .6875rem;
-        padding: 2px 5px 2px 0;
-        color: var(--tertiary-foreground);
-        background: var(--primary-background);
-    }
+	time {
+		margin-top: -2px;
+		font-size: 0.6875rem;
+		line-height: 0.6875rem;
+		padding: 2px 5px 2px 0;
+		color: var(--tertiary-foreground);
+		background: var(--primary-background);
+	}
 
-    ${ props => props.unread && css`
-        border-top: thin solid var(--accent);
-    ` }
+	${(props) =>
+		props.unread &&
+		css`
+			border-top: thin solid var(--accent);
+		`}
 `;
 
 const Unread = styled.div`
-    background: var(--accent);
-    color: white;
-    padding: 5px 8px;
-    border-radius: 60px;
-    font-weight: 600;
+	background: var(--accent);
+	color: white;
+	padding: 5px 8px;
+	border-radius: 60px;
+	font-weight: 600;
 `;
 
 interface Props {
-    date: Date;
-    unread?: boolean;
+	date: Date;
+	unread?: boolean;
 }
 
 export default function DateDivider(props: Props) {
-    return (
-        <Base unread={props.unread}>
-            { props.unread && <Unread>NEW</Unread> }
-            <time>
-                { dayjs(props.date).format("LL") }
-            </time>
-        </Base>
-    );
+	return (
+		<Base unread={props.unread}>
+			{props.unread && <Unread>NEW</Unread>}
+			<time>{dayjs(props.date).format("LL")}</time>
+		</Base>
+	);
 }
diff --git a/src/components/ui/Details.tsx b/src/components/ui/Details.tsx
index f06a67f70a3604faf69a2586d57e35d486d0916c..58ed693514a0f87e696b6b88777287343883a6d2 100644
--- a/src/components/ui/Details.tsx
+++ b/src/components/ui/Details.tsx
@@ -1,68 +1,74 @@
 import styled, { css } from "styled-components";
 
-export default styled.details<{ sticky?: boolean, large?: boolean }>`
-    summary {
-        ${ props => props.sticky && css`
-            top: -1px;
-            z-index: 10;
-            position: sticky;
-        ` }
+export default styled.details<{ sticky?: boolean; large?: boolean }>`
+	summary {
+		${(props) =>
+			props.sticky &&
+			css`
+				top: -1px;
+				z-index: 10;
+				position: sticky;
+			`}
 
-        ${ props => props.large && css`
-            /*padding: 5px 0;*/
-            background: var(--primary-background);
-            color: var(--secondary-foreground);
+		${(props) =>
+			props.large &&
+			css`
+				/*padding: 5px 0;*/
+				background: var(--primary-background);
+				color: var(--secondary-foreground);
 
-            .padding { /*TOFIX: make this applicable only for the friends list menu, DO NOT REMOVE.*/
-                display: flex;
-                align-items: center;
-                padding: 5px 0;
-                margin: 0.8em 0px 0.4em;
-                cursor: pointer;
-            }
-        ` }
+				.padding {
+					/*TOFIX: make this applicable only for the friends list menu, DO NOT REMOVE.*/
+					display: flex;
+					align-items: center;
+					padding: 5px 0;
+					margin: 0.8em 0px 0.4em;
+					cursor: pointer;
+				}
+			`}
 
         outline: none;
-        cursor: pointer;
-        list-style: none;
-        align-items: center;
-        transition: .2s opacity;
-        
-        font-size: 12px;
-        font-weight: 600;
-        text-transform: uppercase;
+		cursor: pointer;
+		list-style: none;
+		align-items: center;
+		transition: 0.2s opacity;
 
-        &::marker, &::-webkit-details-marker {
-            display: none;
-        }
+		font-size: 12px;
+		font-weight: 600;
+		text-transform: uppercase;
 
-        .title {
-            flex-grow: 1;
-            margin-top: 1px;
-            text-overflow: ellipsis;
-            overflow: hidden;
-            white-space: nowrap;
-        }
+		&::marker,
+		&::-webkit-details-marker {
+			display: none;
+		}
 
-        .padding { 
-            display: flex;
-            align-items: center;
+		.title {
+			flex-grow: 1;
+			margin-top: 1px;
+			text-overflow: ellipsis;
+			overflow: hidden;
+			white-space: nowrap;
+		}
 
-            > svg {
-                flex-shrink: 0;
-                margin-inline-end: 4px;
-                transition: .2s ease transform;
-            }
-        }
-    }
+		.padding {
+			display: flex;
+			align-items: center;
 
-    &:not([open]) {
-        summary {
-            opacity: .7;
-        }
-        
-        summary svg {
-            transform: rotateZ(-90deg);
-        }
-    }
+			> svg {
+				flex-shrink: 0;
+				margin-inline-end: 4px;
+				transition: 0.2s ease transform;
+			}
+		}
+	}
+
+	&:not([open]) {
+		summary {
+			opacity: 0.7;
+		}
+
+		summary svg {
+			transform: rotateZ(-90deg);
+		}
+	}
 `;
diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx
index d4e0e3f90f605d2857f5c10b2fe1affc81cc3857..e935b80a99adbcb0de8affbed6a255b546ca49eb 100644
--- a/src/components/ui/Header.tsx
+++ b/src/components/ui/Header.tsx
@@ -1,51 +1,57 @@
 import styled, { css } from "styled-components";
 
 interface Props {
-    borders?: boolean;
-    background?: boolean;
-    placement: 'primary' | 'secondary'
+	borders?: boolean;
+	background?: boolean;
+	placement: "primary" | "secondary";
 }
 
 export default styled.div<Props>`
-    gap: 6px;
-    height: 48px;
-    flex: 0 auto;
-    display: flex;
-    flex-shrink: 0;
-    padding: 0 16px;
-    font-weight: 600;
-    user-select: none;
-    align-items: center;
-
-    background-size: cover !important;
-    background-position: center !important;
-    background-color: var(--primary-header);
-
-    svg {
-        flex-shrink: 0;
-    }
-
-    /*@media only screen and (max-width: 768px) {
+	gap: 6px;
+	height: 48px;
+	flex: 0 auto;
+	display: flex;
+	flex-shrink: 0;
+	padding: 0 16px;
+	font-weight: 600;
+	user-select: none;
+	align-items: center;
+
+	background-size: cover !important;
+	background-position: center !important;
+	background-color: var(--primary-header);
+
+	svg {
+		flex-shrink: 0;
+	}
+
+	/*@media only screen and (max-width: 768px) {
         padding: 0 12px;
     }*/
 
-    @media (pointer: coarse) {
-        height: 56px;
-    }
-    
-    ${ props => props.background && css`
-        height: 120px !important;
-        align-items: flex-end;
-
-        text-shadow: 0px 0px 1px black;
-    ` }
-
-    ${ props => props.placement === 'secondary' && css`
-        background-color: var(--secondary-header);
-        padding: 14px;
-    ` }
-
-    ${ props => props.borders && css`    
-        border-start-start-radius: 8px;
-    ` }
+	@media (pointer: coarse) {
+		height: 56px;
+	}
+
+	${(props) =>
+		props.background &&
+		css`
+			height: 120px !important;
+			align-items: flex-end;
+
+			text-shadow: 0px 0px 1px black;
+		`}
+
+	${(props) =>
+		props.placement === "secondary" &&
+		css`
+			background-color: var(--secondary-header);
+			padding: 14px;
+		`}
+
+    ${(props) =>
+		props.borders &&
+		css`
+			border-start-start-radius: 8px;
+		`}
 `;
diff --git a/src/components/ui/IconButton.tsx b/src/components/ui/IconButton.tsx
index bf946abc275773592ce1654ad7e4876f3adaae8a..3044ec5a6c64ac1c817e282fe8f2bfed36c045c8 100644
--- a/src/components/ui/IconButton.tsx
+++ b/src/components/ui/IconButton.tsx
@@ -1,44 +1,46 @@
 import styled, { css } from "styled-components";
 
 interface Props {
-    type?: 'default' | 'circle'
+	type?: "default" | "circle";
 }
 
 const normal = `var(--secondary-foreground)`;
 const hover = `var(--foreground)`;
 
 export default styled.div<Props>`
-    z-index: 1;
-    display: grid;
-    cursor: pointer;
-    place-items: center;
-    transition: .1s ease background-color;
-
-    fill: ${normal};
-    color: ${normal};
-    /*stroke: ${normal};*/
-
-    a {
-        color: ${normal};
-    }
-
-    &:hover {
-        fill: ${hover};
-        color: ${hover};
-        /*stroke: ${hover};*/
-
-        a {
-            color: ${hover};
-        }
-    }
-
-    ${ props => props.type === 'circle' && css`
-        padding: 4px;
-        border-radius: 50%;
-        background-color: var(--secondary-header);
-
-        &:hover {
-            background-color: var(--primary-header);
-        }
-    ` }
+	z-index: 1;
+	display: grid;
+	cursor: pointer;
+	place-items: center;
+	transition: 0.1s ease background-color;
+
+	fill: ${normal};
+	color: ${normal};
+	/*stroke: ${normal};*/
+
+	a {
+		color: ${normal};
+	}
+
+	&:hover {
+		fill: ${hover};
+		color: ${hover};
+		/*stroke: ${hover};*/
+
+		a {
+			color: ${hover};
+		}
+	}
+
+	${(props) =>
+		props.type === "circle" &&
+		css`
+			padding: 4px;
+			border-radius: 50%;
+			background-color: var(--secondary-header);
+
+			&:hover {
+				background-color: var(--primary-header);
+			}
+		`}
 `;
diff --git a/src/components/ui/InputBox.tsx b/src/components/ui/InputBox.tsx
index 75e3510b9c822161457f4dbfd7cf84019adec716..b6dcbb84bf0df256fa6eaae24a06fba521557cd6 100644
--- a/src/components/ui/InputBox.tsx
+++ b/src/components/ui/InputBox.tsx
@@ -1,39 +1,39 @@
 import styled, { css } from "styled-components";
 
 interface Props {
-    readonly contrast?: boolean;
+	readonly contrast?: boolean;
 }
 
 export default styled.input<Props>`
-    z-index: 1;
-    padding: 8px 16px;
-    border-radius: 6px;
-    
-    font-family: inherit;
-    color: var(--foreground);
-    background: var(--primary-background);
-    transition: 0.2s ease background-color;
-    
-    border: none;
-    outline: 2px solid transparent;
-    transition: outline-color 0.2s ease-in-out;
-
-    &:hover {
-        background: var(--secondary-background);
-    }
-
-    &:focus {
-        outline: 2px solid var(--accent);
-    }
-
-    ${(props) =>
-        props.contrast &&
-        css`
-            color: var(--secondary-foreground);
-            background: var(--secondary-background);
-
-            &:hover {
-                background: var(--hover);
-            }
-        `}
+	z-index: 1;
+	padding: 8px 16px;
+	border-radius: 6px;
+
+	font-family: inherit;
+	color: var(--foreground);
+	background: var(--primary-background);
+	transition: 0.2s ease background-color;
+
+	border: none;
+	outline: 2px solid transparent;
+	transition: outline-color 0.2s ease-in-out;
+
+	&:hover {
+		background: var(--secondary-background);
+	}
+
+	&:focus {
+		outline: 2px solid var(--accent);
+	}
+
+	${(props) =>
+		props.contrast &&
+		css`
+			color: var(--secondary-foreground);
+			background: var(--secondary-background);
+
+			&:hover {
+				background: var(--hover);
+			}
+		`}
 `;
diff --git a/src/components/ui/LineDivider.tsx b/src/components/ui/LineDivider.tsx
index 58a9c7a45817feddd3bd9eb9aa9bc1b73dc2dd63..0ffd1e7c9532028b58181a06240d848a006649a3 100644
--- a/src/components/ui/LineDivider.tsx
+++ b/src/components/ui/LineDivider.tsx
@@ -1,9 +1,9 @@
 import styled from "styled-components";
 
 export default styled.div`
-    height: 0px;
-    opacity: 0.6;
-    flex-shrink: 0;
-    margin: 8px 10px;
-    border-top: 1px solid var(--tertiary-foreground);
+	height: 0px;
+	opacity: 0.6;
+	flex-shrink: 0;
+	margin: 8px 10px;
+	border-top: 1px solid var(--tertiary-foreground);
 `;
diff --git a/src/components/ui/Masks.tsx b/src/components/ui/Masks.tsx
index eba85108a99bd53b8845392b888f1a9132579150..d8cadc71d08db113c242a345a0210137e846afb9 100644
--- a/src/components/ui/Masks.tsx
+++ b/src/components/ui/Masks.tsx
@@ -1,22 +1,22 @@
 // This file must be imported and used at least once for SVG masks.
 
 export default function Masks() {
-    return (
-        <svg width={0} height={0} style={{ position: 'fixed' }}>
-            <defs>
-                <mask id="server">
-                    <rect x="0" y="0" width="32" height="32" fill="white" />
-                    <circle cx="27" cy="5" r="7" fill={"black"} />
-                </mask>
-                <mask id="user">
-                    <rect x="0" y="0" width="32" height="32" fill="white" />
-                    <circle cx="27" cy="27" r="7" fill={"black"} />
-                </mask>
-                <mask id="overlap">
-                    <rect x="0" y="0" width="32" height="32" fill="white" />
-                    <circle cx="32" cy="16" r="18" fill={"black"} />
-                </mask>
-            </defs>
-        </svg>
-    )
-}
\ No newline at end of file
+	return (
+		<svg width={0} height={0} style={{ position: "fixed" }}>
+			<defs>
+				<mask id="server">
+					<rect x="0" y="0" width="32" height="32" fill="white" />
+					<circle cx="27" cy="5" r="7" fill={"black"} />
+				</mask>
+				<mask id="user">
+					<rect x="0" y="0" width="32" height="32" fill="white" />
+					<circle cx="27" cy="27" r="7" fill={"black"} />
+				</mask>
+				<mask id="overlap">
+					<rect x="0" y="0" width="32" height="32" fill="white" />
+					<circle cx="32" cy="16" r="18" fill={"black"} />
+				</mask>
+			</defs>
+		</svg>
+	);
+}
diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx
index 6926508780f3c50cc3a7c5c15d94de71aa15e44c..57a43bf715581094d4a2ac25b934f180fae9da28 100644
--- a/src/components/ui/Modal.tsx
+++ b/src/components/ui/Modal.tsx
@@ -1,8 +1,10 @@
-import Button from "./Button";
+import styled, { css, keyframes } from "styled-components";
+
 import classNames from "classnames";
-import { Children } from "../../types/Preact";
 import { createPortal, useEffect } from "preact/compat";
-import styled, { css, keyframes } from "styled-components";
+
+import { Children } from "../../types/Preact";
+import Button from "./Button";
 
 const open = keyframes`
     0% {opacity: 0;}
@@ -17,168 +19,181 @@ const zoomIn = keyframes`
 `;
 
 const ModalBase = styled.div`
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    z-index: 9999;
-    position: fixed;
-    max-height: 100%;
-    user-select: none;
-
-    animation-name: ${open};
-    animation-duration: 0.2s;
-
-    display: grid;
-    overflow-y: auto;
-    place-items: center;
-
-    color: var(--foreground);
-    background: rgba(0, 0, 0, 0.8);
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 9999;
+	position: fixed;
+	max-height: 100%;
+	user-select: none;
+
+	animation-name: ${open};
+	animation-duration: 0.2s;
+
+	display: grid;
+	overflow-y: auto;
+	place-items: center;
+
+	color: var(--foreground);
+	background: rgba(0, 0, 0, 0.8);
 `;
 
 const ModalContainer = styled.div`
-    overflow: hidden;
-    border-radius: 8px;
-    max-width: calc(100vw - 20px);
+	overflow: hidden;
+	border-radius: 8px;
+	max-width: calc(100vw - 20px);
 
-    animation-name: ${zoomIn};
-    animation-duration: 0.25s;
-    animation-timing-function: cubic-bezier(.3,.3,.18,1.1);
+	animation-name: ${zoomIn};
+	animation-duration: 0.25s;
+	animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
 `;
 
-const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border' | 'padding']?: boolean }>`
-    border-radius: 8px;
-    text-overflow: ellipsis;
-
-    h3 {
-        margin-top: 0;
-    }
-
-    form {
-        display: flex;
-        flex-direction: column;
-    }
-
-    ${ props => !props.noBackground && css`
-        background: var(--secondary-header);
-    ` }
-
-    ${ props => props.padding && css`
-        padding: 1.5em;
-    ` }
-
-    ${ props => props.attachment && css`
-        border-radius: 8px 8px 0 0;
-    ` }
-
-    ${ props => props.border && css`
-        border-radius: 10px;
-        border: 2px solid var(--secondary-background);
-    ` }
+const ModalContent = styled.div<
+	{ [key in "attachment" | "noBackground" | "border" | "padding"]?: boolean }
+>`
+	border-radius: 8px;
+	text-overflow: ellipsis;
+
+	h3 {
+		margin-top: 0;
+	}
+
+	form {
+		display: flex;
+		flex-direction: column;
+	}
+
+	${(props) =>
+		!props.noBackground &&
+		css`
+			background: var(--secondary-header);
+		`}
+
+	${(props) =>
+		props.padding &&
+		css`
+			padding: 1.5em;
+		`}
+
+    ${(props) =>
+		props.attachment &&
+		css`
+			border-radius: 8px 8px 0 0;
+		`}
+
+    ${(props) =>
+		props.border &&
+		css`
+			border-radius: 10px;
+			border: 2px solid var(--secondary-background);
+		`}
 `;
 
 const ModalActions = styled.div`
-    gap: 8px;
-    display: flex;
-    flex-direction: row-reverse;
+	gap: 8px;
+	display: flex;
+	flex-direction: row-reverse;
 
-    padding: 1em 1.5em;
-    border-radius: 0 0 8px 8px;
-    background: var(--secondary-background);
+	padding: 1em 1.5em;
+	border-radius: 0 0 8px 8px;
+	background: var(--secondary-background);
 `;
 
 export interface Action {
-    text: Children;
-    onClick: () => void;
-    confirmation?: boolean;
-    contrast?: boolean;
-    error?: boolean;
+	text: Children;
+	onClick: () => void;
+	confirmation?: boolean;
+	contrast?: boolean;
+	error?: boolean;
 }
 
 interface Props {
-    children?: Children;
-    title?: Children;
-
-    disallowClosing?: boolean;
-    noBackground?: boolean;
-    dontModal?: boolean;
-    padding?: boolean;
-
-    onClose: () => void;
-    actions?: Action[];
-    disabled?: boolean;
-    border?: boolean;
-    visible: boolean;
+	children?: Children;
+	title?: Children;
+
+	disallowClosing?: boolean;
+	noBackground?: boolean;
+	dontModal?: boolean;
+	padding?: boolean;
+
+	onClose: () => void;
+	actions?: Action[];
+	disabled?: boolean;
+	border?: boolean;
+	visible: boolean;
 }
 
 export default function Modal(props: Props) {
-    if (!props.visible) return null;
-
-    let content = (
-        <ModalContent
-            attachment={!!props.actions}
-            noBackground={props.noBackground}
-            border={props.border}
-            padding={props.padding ?? !props.dontModal}>
-            {props.title && <h3>{props.title}</h3>}
-            {props.children}
-        </ModalContent>
-    );
-
-    if (props.dontModal) {
-        return content;
-    }
-
-    useEffect(() => {
-        if (props.disallowClosing) return;
-
-        function keyDown(e: KeyboardEvent) {
-            if (e.key === "Escape") {
-                props.onClose();
-            }
-        }
-
-        document.body.addEventListener("keydown", keyDown);
-        return () => document.body.removeEventListener("keydown", keyDown);
-    }, [ props.disallowClosing, props.onClose ]);
-
-    let confirmationAction = props.actions?.find(action => action.confirmation);
-    useEffect(() => {
-        if (!confirmationAction) return;
-
-        // ! FIXME: this may be done better if we
-        // ! can focus the button although that
-        // ! doesn't seem to work...
-        function keyDown(e: KeyboardEvent) {
-            if (e.key === "Enter") {
-                confirmationAction!.onClick();
-            }
-        }
-
-        document.body.addEventListener("keydown", keyDown);
-        return () => document.body.removeEventListener("keydown", keyDown);
-    }, [ confirmationAction ]);
-
-    return createPortal(
-        <ModalBase onClick={(!props.disallowClosing && props.onClose) || undefined}>
-            <ModalContainer onClick={e => (e.cancelBubble = true)}>
-                {content}
-                {props.actions && (
-                    <ModalActions>
-                        {props.actions.map(x => (
-                            <Button
-                                contrast={x.contrast ?? true}
-                                error={x.error ?? false}
-                                onClick={x.onClick}
-                                disabled={props.disabled}>
-                                {x.text}
-                            </Button>
-                        ))}
-                    </ModalActions>
-                )}
-            </ModalContainer>
-        </ModalBase>,
-        document.body
-    );
+	if (!props.visible) return null;
+
+	let content = (
+		<ModalContent
+			attachment={!!props.actions}
+			noBackground={props.noBackground}
+			border={props.border}
+			padding={props.padding ?? !props.dontModal}>
+			{props.title && <h3>{props.title}</h3>}
+			{props.children}
+		</ModalContent>
+	);
+
+	if (props.dontModal) {
+		return content;
+	}
+
+	useEffect(() => {
+		if (props.disallowClosing) return;
+
+		function keyDown(e: KeyboardEvent) {
+			if (e.key === "Escape") {
+				props.onClose();
+			}
+		}
+
+		document.body.addEventListener("keydown", keyDown);
+		return () => document.body.removeEventListener("keydown", keyDown);
+	}, [props.disallowClosing, props.onClose]);
+
+	let confirmationAction = props.actions?.find(
+		(action) => action.confirmation,
+	);
+	useEffect(() => {
+		if (!confirmationAction) return;
+
+		// ! FIXME: this may be done better if we
+		// ! can focus the button although that
+		// ! doesn't seem to work...
+		function keyDown(e: KeyboardEvent) {
+			if (e.key === "Enter") {
+				confirmationAction!.onClick();
+			}
+		}
+
+		document.body.addEventListener("keydown", keyDown);
+		return () => document.body.removeEventListener("keydown", keyDown);
+	}, [confirmationAction]);
+
+	return createPortal(
+		<ModalBase
+			onClick={(!props.disallowClosing && props.onClose) || undefined}>
+			<ModalContainer onClick={(e) => (e.cancelBubble = true)}>
+				{content}
+				{props.actions && (
+					<ModalActions>
+						{props.actions.map((x) => (
+							<Button
+								contrast={x.contrast ?? true}
+								error={x.error ?? false}
+								onClick={x.onClick}
+								disabled={props.disabled}>
+								{x.text}
+							</Button>
+						))}
+					</ModalActions>
+				)}
+			</ModalContainer>
+		</ModalBase>,
+		document.body,
+	);
 }
diff --git a/src/components/ui/Overline.tsx b/src/components/ui/Overline.tsx
index 171590770c6ae480838b913a71f49260a60bea50..8dc0b54e684036e9f02a4cce5cf057b148e56af2 100644
--- a/src/components/ui/Overline.tsx
+++ b/src/components/ui/Overline.tsx
@@ -1,58 +1,64 @@
 import styled, { css } from "styled-components";
+
+import { Text } from "preact-i18n";
+
 import { Children } from "../../types/Preact";
-import { Text } from 'preact-i18n';
-
-type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'as'> & {
-    error?: string;
-    block?: boolean;
-    spaced?: boolean;
-    children?: Children;
-    type?: "default" | "subtle" | "error";
-}
+
+type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, "children" | "as"> & {
+	error?: string;
+	block?: boolean;
+	spaced?: boolean;
+	children?: Children;
+	type?: "default" | "subtle" | "error";
+};
 
 const OverlineBase = styled.div<Omit<Props, "children" | "error">>`
-    display: inline;
-    margin: 0.4em 0;
+	display: inline;
+	margin: 0.4em 0;
 
-    ${ props => props.spaced && css`
-        margin-top: 0.8em;
-    ` }
+	${(props) =>
+		props.spaced &&
+		css`
+			margin-top: 0.8em;
+		`}
 
-    font-size: 14px;
-    font-weight: 600;
-    color: var(--foreground);
-    text-transform: uppercase;
+	font-size: 14px;
+	font-weight: 600;
+	color: var(--foreground);
+	text-transform: uppercase;
 
-    ${(props) =>
-        props.type === "subtle" &&
-        css`
-            font-size: 12px;
-            color: var(--secondary-foreground);
-        `}
+	${(props) =>
+		props.type === "subtle" &&
+		css`
+			font-size: 12px;
+			color: var(--secondary-foreground);
+		`}
 
-    ${(props) =>
-        props.type === "error" &&
-        css`
-            font-size: 12px;
-            font-weight: 400;
-            color: var(--error);
-        `}
+	${(props) =>
+		props.type === "error" &&
+		css`
+			font-size: 12px;
+			font-weight: 400;
+			color: var(--error);
+		`}
 
     ${(props) =>
-        props.block &&
-        css`
-            display: block;
-        `}
+		props.block &&
+		css`
+			display: block;
+		`}
 `;
 
 export default function Overline(props: Props) {
-    return (
-        <OverlineBase {...props}>
-            {props.children}
-            {props.children && props.error && <> &middot; </>}
-            {props.error && <Overline type="error">
-                <Text id={`error.${props.error}`}>{props.error}</Text>
-            </Overline>}
-        </OverlineBase>
-    );
+	return (
+		<OverlineBase {...props}>
+			{props.children}
+			{props.children && props.error && <> &middot; </>}
+			{props.error && (
+				<Overline type="error">
+					<Text id={`error.${props.error}`}>{props.error}</Text>
+				</Overline>
+			)}
+		</OverlineBase>
+	);
 }
diff --git a/src/components/ui/Preloader.tsx b/src/components/ui/Preloader.tsx
index 7e3ec3fcda41739da3f21da363f692a212d1aeda..a9e70ab4e1224fd3fb903e14d35b97aadd7f1729 100644
--- a/src/components/ui/Preloader.tsx
+++ b/src/components/ui/Preloader.tsx
@@ -21,83 +21,83 @@ const prRing = keyframes`
 `;
 
 const PreloaderBase = styled.div`
-    width: 100%;
-    height: 100%;
-
-    display: grid;
-    place-items: center;
-
-    .spinner {
-        width: 58px;
-        display: flex;
-        text-align: center;
-        margin: 100px auto 0;
-        justify-content: space-between;
-    }
-
-    .spinner > div {
-        width: 14px;
-        height: 14px;
-        background-color: var(--tertiary-foreground);
-
-        border-radius: 100%;
-        display: inline-block;
-        animation: ${skSpinner} 1.4s infinite ease-in-out both;
-    }
-
-    .spinner div:nth-child(1) {
-        animation-delay: -0.32s;
-    }
-
-    .spinner div:nth-child(2) {
-        animation-delay: -0.16s;
-    }
-
-    .ring {
-        display: inline-block;
-        position: relative;
-        width: 48px;
-        height: 52px;
-    }
-
-    .ring div {
-        width: 32px;
-        margin: 8px;
-        height: 32px;
-        display: block;
-        position: absolute;
-        border-radius: 50%;
-        box-sizing: border-box;
-        border: 2px solid #fff;
-        animation: ${prRing} 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
-        border-color: #fff transparent transparent transparent;
-    }
-
-    .ring div:nth-child(1) {
-        animation-delay: -0.45s;
-    }
-
-    .ring div:nth-child(2) {
-        animation-delay: -0.3s;
-    }
-
-    .ring div:nth-child(3) {
-        animation-delay: -0.15s;
-    }
+	width: 100%;
+	height: 100%;
+
+	display: grid;
+	place-items: center;
+
+	.spinner {
+		width: 58px;
+		display: flex;
+		text-align: center;
+		margin: 100px auto 0;
+		justify-content: space-between;
+	}
+
+	.spinner > div {
+		width: 14px;
+		height: 14px;
+		background-color: var(--tertiary-foreground);
+
+		border-radius: 100%;
+		display: inline-block;
+		animation: ${skSpinner} 1.4s infinite ease-in-out both;
+	}
+
+	.spinner div:nth-child(1) {
+		animation-delay: -0.32s;
+	}
+
+	.spinner div:nth-child(2) {
+		animation-delay: -0.16s;
+	}
+
+	.ring {
+		display: inline-block;
+		position: relative;
+		width: 48px;
+		height: 52px;
+	}
+
+	.ring div {
+		width: 32px;
+		margin: 8px;
+		height: 32px;
+		display: block;
+		position: absolute;
+		border-radius: 50%;
+		box-sizing: border-box;
+		border: 2px solid #fff;
+		animation: ${prRing} 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+		border-color: #fff transparent transparent transparent;
+	}
+
+	.ring div:nth-child(1) {
+		animation-delay: -0.45s;
+	}
+
+	.ring div:nth-child(2) {
+		animation-delay: -0.3s;
+	}
+
+	.ring div:nth-child(3) {
+		animation-delay: -0.15s;
+	}
 `;
 
 interface Props {
-    type: 'spinner' | 'ring'
+	type: "spinner" | "ring";
 }
 
 export default function Preloader({ type }: Props) {
-    return (
-        <PreloaderBase>
-            <div class={type}>
-                <div />
-                <div />
-                <div />
-            </div>
-        </PreloaderBase>
-    );
+	return (
+		<PreloaderBase>
+			<div class={type}>
+				<div />
+				<div />
+				<div />
+			</div>
+		</PreloaderBase>
+	);
 }
diff --git a/src/components/ui/Radio.tsx b/src/components/ui/Radio.tsx
index e988bbc94cd113034f2cce09f714ed3df8d20dc5..7bd220c488f8a6a879f6d01bcafbb4560ec65d7a 100644
--- a/src/components/ui/Radio.tsx
+++ b/src/components/ui/Radio.tsx
@@ -1,111 +1,111 @@
-import { Children } from "../../types/Preact";
-import styled, { css } from "styled-components";
 import { Circle } from "@styled-icons/boxicons-regular";
+import styled, { css } from "styled-components";
+
+import { Children } from "../../types/Preact";
 
 interface Props {
-    children: Children;
-    description?: Children;
+	children: Children;
+	description?: Children;
 
-    checked: boolean;
-    disabled?: boolean;
-    onSelect: () => void;
+	checked: boolean;
+	disabled?: boolean;
+	onSelect: () => void;
 }
 
 interface BaseProps {
-    selected: boolean;
+	selected: boolean;
 }
 
 const RadioBase = styled.label<BaseProps>`
-    gap: 4px;
-    z-index: 1;
-    padding: 4px;
-    display: flex;
-    cursor: pointer;
-    align-items: center;
-
-    font-size: 1rem;
-    font-weight: 600;
-    user-select: none;
-    border-radius: 4px;
-    transition: 0.2s ease all;
-
-    &:hover {
-        background: var(--hover);
-    }
-
-    > input {
-        display: none;
-    }
-
-    > div {
-        margin: 4px;
-        width: 24px;
-        height: 24px;
-        display: grid;
-        border-radius: 50%;
-        place-items: center;
-        background: var(--foreground);
-
-        svg {
-            color: var(--foreground);
-            /*stroke-width: 2;*/
-        }
-    }
-
-    ${(props) =>
-        props.selected &&
-        css`
-            color: white;
-            cursor: default;
-            background: var(--accent);
-
-            > div {
-                background: white;
-            }
-
-            > div svg {
-                color: var(--accent);
-            }
-
-            &:hover {
-                background: var(--accent);
-            }
-        `}
+	gap: 4px;
+	z-index: 1;
+	padding: 4px;
+	display: flex;
+	cursor: pointer;
+	align-items: center;
+
+	font-size: 1rem;
+	font-weight: 600;
+	user-select: none;
+	border-radius: 4px;
+	transition: 0.2s ease all;
+
+	&:hover {
+		background: var(--hover);
+	}
+
+	> input {
+		display: none;
+	}
+
+	> div {
+		margin: 4px;
+		width: 24px;
+		height: 24px;
+		display: grid;
+		border-radius: 50%;
+		place-items: center;
+		background: var(--foreground);
+
+		svg {
+			color: var(--foreground);
+			/*stroke-width: 2;*/
+		}
+	}
+
+	${(props) =>
+		props.selected &&
+		css`
+			color: white;
+			cursor: default;
+			background: var(--accent);
+
+			> div {
+				background: white;
+			}
+
+			> div svg {
+				color: var(--accent);
+			}
+
+			&:hover {
+				background: var(--accent);
+			}
+		`}
 `;
 
 const RadioDescription = styled.span<BaseProps>`
-    font-size: 0.8em;
-    font-weight: 400;
-    color: var(--secondary-foreground);
-
-    ${(props) =>
-        props.selected &&
-        css`
-            color: white;
-        `}
+	font-size: 0.8em;
+	font-weight: 400;
+	color: var(--secondary-foreground);
+
+	${(props) =>
+		props.selected &&
+		css`
+			color: white;
+		`}
 `;
 
 export default function Radio(props: Props) {
-    return (
-        <RadioBase
-            selected={props.checked}
-            disabled={props.disabled}
-            onClick={() =>
-                !props.disabled && props.onSelect && props.onSelect()
-            }
-        >
-            <div>
-                <Circle size={12} />
-            </div>
-            <input type="radio" checked={props.checked} />
-            <span>
-                <span>{props.children}</span>
-                {props.description && (
-                    <RadioDescription selected={props.checked}>
-                        {props.description}
-                    </RadioDescription>
-                )}
-            </span>
-        </RadioBase>
-    );
+	return (
+		<RadioBase
+			selected={props.checked}
+			disabled={props.disabled}
+			onClick={() =>
+				!props.disabled && props.onSelect && props.onSelect()
+			}>
+			<div>
+				<Circle size={12} />
+			</div>
+			<input type="radio" checked={props.checked} />
+			<span>
+				<span>{props.children}</span>
+				{props.description && (
+					<RadioDescription selected={props.checked}>
+						{props.description}
+					</RadioDescription>
+				)}
+			</span>
+		</RadioBase>
+	);
 }
diff --git a/src/components/ui/TextArea.tsx b/src/components/ui/TextArea.tsx
index 88f5959dff523b78a3f84c8086e0c10db0dc0e9e..3d3f296bfb769fe564a35a131070ab3f360946b6 100644
--- a/src/components/ui/TextArea.tsx
+++ b/src/components/ui/TextArea.tsx
@@ -1,10 +1,10 @@
 import styled, { css } from "styled-components";
 
 export interface TextAreaProps {
-    code?: boolean;
-    padding?: number;
-    lineHeight?: number;
-    hideBorder?: boolean;
+	code?: boolean;
+	padding?: number;
+	lineHeight?: number;
+	hideBorder?: boolean;
 }
 
 export const TEXT_AREA_BORDER_WIDTH = 2;
@@ -12,37 +12,46 @@ export const DEFAULT_TEXT_AREA_PADDING = 16;
 export const DEFAULT_LINE_HEIGHT = 20;
 
 export default styled.textarea<TextAreaProps>`
-    width: 100%;
-    resize: none;
-    display: block;
-    color: var(--foreground);
-    background: var(--secondary-background);
-    padding: ${ props => props.padding ?? DEFAULT_TEXT_AREA_PADDING }px;
-    line-height: ${ props => props.lineHeight ?? DEFAULT_LINE_HEIGHT }px;
-
-    ${ props => props.hideBorder && css`
-        border: none;
-    ` }
-
-    ${ props => !props.hideBorder && css`
-        border-radius: 4px;
-        transition: border-color .2s ease-in-out;
-        border: ${TEXT_AREA_BORDER_WIDTH}px solid transparent;
-    ` }
+	width: 100%;
+	resize: none;
+	display: block;
+	color: var(--foreground);
+	background: var(--secondary-background);
+	padding: ${(props) => props.padding ?? DEFAULT_TEXT_AREA_PADDING}px;
+	line-height: ${(props) => props.lineHeight ?? DEFAULT_LINE_HEIGHT}px;
+
+	${(props) =>
+		props.hideBorder &&
+		css`
+			border: none;
+		`}
+
+	${(props) =>
+		!props.hideBorder &&
+		css`
+			border-radius: 4px;
+			transition: border-color 0.2s ease-in-out;
+			border: ${TEXT_AREA_BORDER_WIDTH}px solid transparent;
+		`}
 
     &:focus {
-        outline: none;
-
-        ${ props => !props.hideBorder && css`
-            border: ${TEXT_AREA_BORDER_WIDTH}px solid var(--accent);
-        ` }
-    }
-
-    ${ props => props.code ? css`
-        font-family: var(--monoscape-font-font), monospace;
-    ` : css`
-        font-family: inherit;
-    ` }
-    
-    font-variant-ligatures: var(--ligatures);
+		outline: none;
+
+		${(props) =>
+			!props.hideBorder &&
+			css`
+				border: ${TEXT_AREA_BORDER_WIDTH}px solid var(--accent);
+			`}
+	}
+
+	${(props) =>
+		props.code
+			? css`
+					font-family: var(--monoscape-font-font), monospace;
+			  `
+			: css`
+					font-family: inherit;
+			  `}
+
+	font-variant-ligatures: var(--ligatures);
 `;
diff --git a/src/components/ui/Tip.tsx b/src/components/ui/Tip.tsx
index f664a58cac3965f6632a73a437ab295df0ef13f3..0ef02c093b423acd984720203b285a3ea5c61cb3 100644
--- a/src/components/ui/Tip.tsx
+++ b/src/components/ui/Tip.tsx
@@ -1,65 +1,69 @@
-import { Children } from "../../types/Preact";
-import styled, { css } from "styled-components";
 import { InfoCircle } from "@styled-icons/boxicons-regular";
+import styled, { css } from "styled-components";
+
+import { Children } from "../../types/Preact";
 
 interface Props {
-    warning?: boolean
-    error?: boolean
+	warning?: boolean;
+	error?: boolean;
 }
 
 export const Separator = styled.div<Props>`
-    height: 1px;
-    width: calc(100% - 10px);
-    background: var(--secondary-header);
-    margin: 18px auto;
+	height: 1px;
+	width: calc(100% - 10px);
+	background: var(--secondary-header);
+	margin: 18px auto;
 `;
 
 export const TipBase = styled.div<Props>`
-    display: flex;
-    padding: 12px;
-    overflow: hidden;
-    align-items: center;
+	display: flex;
+	padding: 12px;
+	overflow: hidden;
+	align-items: center;
 
-    font-size: 14px;
-    border-radius: 7px;
-    background: var(--primary-header);
-    border: 2px solid var(--secondary-header);
+	font-size: 14px;
+	border-radius: 7px;
+	background: var(--primary-header);
+	border: 2px solid var(--secondary-header);
 
-    a {
-        cursor: pointer;
-        &:hover {
-            text-decoration: underline;
-        }
-    }
+	a {
+		cursor: pointer;
+		&:hover {
+			text-decoration: underline;
+		}
+	}
 
-    svg {
-        flex-shrink: 0;
-        margin-inline-end: 10px;
-    }
+	svg {
+		flex-shrink: 0;
+		margin-inline-end: 10px;
+	}
 
-    ${ props => props.warning && css`
-        color: var(--warning);
-        border: 2px solid var(--warning);
-        background: var(--secondary-header);
-    ` }
+	${(props) =>
+		props.warning &&
+		css`
+			color: var(--warning);
+			border: 2px solid var(--warning);
+			background: var(--secondary-header);
+		`}
 
-    ${ props => props.error && css`
-        color: var(--error);
-        border: 2px solid var(--error);
-        background: var(--secondary-header);
-    ` }
+	${(props) =>
+		props.error &&
+		css`
+			color: var(--error);
+			border: 2px solid var(--error);
+			background: var(--secondary-header);
+		`}
 `;
 
 export default function Tip(props: Props & { children: Children }) {
-    const { children, ...tipProps } = props;
-    return (
-        <>
-            <Separator />
-            <TipBase {...tipProps}>
-                <InfoCircle size={20} />
-                <span>{props.children}</span>
-            </TipBase>
-        </>
-        
-    );
+	const { children, ...tipProps } = props;
+	return (
+		<>
+			<Separator />
+			<TipBase {...tipProps}>
+				<InfoCircle size={20} />
+				<span>{props.children}</span>
+			</TipBase>
+		</>
+	);
 }
diff --git a/src/context/Locale.tsx b/src/context/Locale.tsx
index 6fdff9e216a3173b2e8e6eedac9b9744d9d70d6e..b9200e38f0bc162bfbdfadc33ee011d9d0764f67 100644
--- a/src/context/Locale.tsx
+++ b/src/context/Locale.tsx
@@ -1,186 +1,217 @@
-import { IntlProvider } from "preact-i18n";
+import dayjs from "dayjs";
+import calendar from "dayjs/plugin/calendar";
+import format from "dayjs/plugin/localizedFormat";
+import update from "dayjs/plugin/updateLocale";
 import defaultsDeep from "lodash.defaultsdeep";
-import { connectState } from "../redux/connector";
+
+import { IntlProvider } from "preact-i18n";
 import { useEffect, useState } from "preact/hooks";
+
+import { connectState } from "../redux/connector";
+
 import definition from "../../external/lang/en.json";
 
-import dayjs from "dayjs";
-import calendar from "dayjs/plugin/calendar";
-import update from "dayjs/plugin/updateLocale";
-import format from "dayjs/plugin/localizedFormat";
 dayjs.extend(calendar);
 dayjs.extend(format);
 dayjs.extend(update);
 
 export enum Language {
-    ENGLISH = "en",
-
-    ARABIC = "ar",
-    AZERBAIJANI = "az",
-    CZECH = "cs",
-    GERMAN = "de",
-    SPANISH = "es",
-    FINNISH = "fi",
-    FRENCH = "fr",
-    HINDI = "hi",
-    CROATIAN = "hr",
-    HUNGARIAN = "hu",
-    INDONESIAN = "id",
-    LITHUANIAN = "lt",
-    MACEDONIAN = "mk",
-    DUTCH = "nl",
-    POLISH = "pl",
-    PORTUGUESE_BRAZIL = "pt_BR",
-    ROMANIAN = "ro",
-    RUSSIAN = "ru",
-    SERBIAN = "sr",
-    SWEDISH = "sv",
-    TURKISH = "tr",
-    UKRANIAN = "uk",
-    CHINESE_SIMPLIFIED = "zh_Hans",
-
-    OWO = "owo",
-    PIRATE = "pr",
-    BOTTOM = "bottom",
-    PIGLATIN = "piglatin",
+	ENGLISH = "en",
+
+	ARABIC = "ar",
+	AZERBAIJANI = "az",
+	CZECH = "cs",
+	GERMAN = "de",
+	SPANISH = "es",
+	FINNISH = "fi",
+	FRENCH = "fr",
+	HINDI = "hi",
+	CROATIAN = "hr",
+	HUNGARIAN = "hu",
+	INDONESIAN = "id",
+	LITHUANIAN = "lt",
+	MACEDONIAN = "mk",
+	DUTCH = "nl",
+	POLISH = "pl",
+	PORTUGUESE_BRAZIL = "pt_BR",
+	ROMANIAN = "ro",
+	RUSSIAN = "ru",
+	SERBIAN = "sr",
+	SWEDISH = "sv",
+	TURKISH = "tr",
+	UKRANIAN = "uk",
+	CHINESE_SIMPLIFIED = "zh_Hans",
+
+	OWO = "owo",
+	PIRATE = "pr",
+	BOTTOM = "bottom",
+	PIGLATIN = "piglatin",
 }
 
 export interface LanguageEntry {
-    display: string;
-    emoji: string;
-    i18n: string;
-    dayjs?: string;
-    rtl?: boolean;
-    alt?: boolean;
+	display: string;
+	emoji: string;
+	i18n: string;
+	dayjs?: string;
+	rtl?: boolean;
+	alt?: boolean;
 }
 
 export const Languages: { [key in Language]: LanguageEntry } = {
-    en: {
-        display: "English (Traditional)",
-        emoji: "🇬🇧",
-        i18n: "en",
-        dayjs: "en-gb",
-    },
-
-    ar: { display: "عربي", emoji: "🇸🇦", i18n: "ar", rtl: true },
-    az: { display: "Azərbaycan dili", emoji: "🇦🇿", i18n: "az" },
-    cs: { display: "Čeština", emoji: "🇨🇿", i18n: "cs" },
-    de: { display: "Deutsch", emoji: "🇩🇪", i18n: "de" },
-    es: { display: "Español", emoji: "🇪🇸", i18n: "es" },
-    fi: { display: "suomi", emoji: "🇫🇮", i18n: "fi" },
-    fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" },
-    hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" },
-    hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" },
-    hu: { display: "magyar", emoji: "🇭🇺", i18n: "hu" },
-    id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" },
-    lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" },
-    mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" },
-    nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" },
-    pl: { display: "Polski", emoji: "🇵🇱", i18n: "pl" },
-    pt_BR: {
-        display: "Português (do Brasil)",
-        emoji: "🇧🇷",
-        i18n: "pt_BR",
-        dayjs: "pt-br",
-    },
-    ro: { display: "Română", emoji: "🇷🇴", i18n: "ro" },
-    ru: { display: "Русский", emoji: "🇷🇺", i18n: "ru" },
-    sr: { display: "Српски", emoji: "🇷🇸", i18n: "sr" },
-    sv: { display: "Svenska", emoji: "🇸🇪", i18n: "sv" },
-    tr: { display: "Türkçe", emoji: "🇹🇷", i18n: "tr" },
-    uk: { display: "Українська", emoji: "🇺🇦", i18n: "uk" },
-    zh_Hans: {
-        display: "中文 (简体)",
-        emoji: "🇨🇳",
-        i18n: "zh_Hans",
-        dayjs: "zh",
-    },
-
-    owo: { display: "OwO", emoji: "🐱", i18n: "owo", dayjs: "en-gb", alt: true },
-    pr: { display: "Pirate", emoji: "🏴‍☠️", i18n: "pr", dayjs: "en-gb", alt: true },
-    bottom: { display: "Bottom", emoji: "🥺", i18n: "bottom", dayjs: "en-gb", alt: true },
-    piglatin: {
-        display: "Pig Latin",
-        emoji: "🐖",
-        i18n: "piglatin",
-        dayjs: "en-gb",
-        alt: true
-    },
+	en: {
+		display: "English (Traditional)",
+		emoji: "🇬🇧",
+		i18n: "en",
+		dayjs: "en-gb",
+	},
+
+	ar: { display: "عربي", emoji: "🇸🇦", i18n: "ar", rtl: true },
+	az: { display: "Azərbaycan dili", emoji: "🇦🇿", i18n: "az" },
+	cs: { display: "Čeština", emoji: "🇨🇿", i18n: "cs" },
+	de: { display: "Deutsch", emoji: "🇩🇪", i18n: "de" },
+	es: { display: "Español", emoji: "🇪🇸", i18n: "es" },
+	fi: { display: "suomi", emoji: "🇫🇮", i18n: "fi" },
+	fr: { display: "Français", emoji: "🇫🇷", i18n: "fr" },
+	hi: { display: "हिन्दी", emoji: "🇮🇳", i18n: "hi" },
+	hr: { display: "Hrvatski", emoji: "🇭🇷", i18n: "hr" },
+	hu: { display: "magyar", emoji: "🇭🇺", i18n: "hu" },
+	id: { display: "bahasa Indonesia", emoji: "🇮🇩", i18n: "id" },
+	lt: { display: "Lietuvių", emoji: "🇱🇹", i18n: "lt" },
+	mk: { display: "Македонски", emoji: "🇲🇰", i18n: "mk" },
+	nl: { display: "Nederlands", emoji: "🇳🇱", i18n: "nl" },
+	pl: { display: "Polski", emoji: "🇵🇱", i18n: "pl" },
+	pt_BR: {
+		display: "Português (do Brasil)",
+		emoji: "🇧🇷",
+		i18n: "pt_BR",
+		dayjs: "pt-br",
+	},
+	ro: { display: "Română", emoji: "🇷🇴", i18n: "ro" },
+	ru: { display: "Русский", emoji: "🇷🇺", i18n: "ru" },
+	sr: { display: "Српски", emoji: "🇷🇸", i18n: "sr" },
+	sv: { display: "Svenska", emoji: "🇸🇪", i18n: "sv" },
+	tr: { display: "Türkçe", emoji: "🇹🇷", i18n: "tr" },
+	uk: { display: "Українська", emoji: "🇺🇦", i18n: "uk" },
+	zh_Hans: {
+		display: "中文 (简体)",
+		emoji: "🇨🇳",
+		i18n: "zh_Hans",
+		dayjs: "zh",
+	},
+
+	owo: {
+		display: "OwO",
+		emoji: "🐱",
+		i18n: "owo",
+		dayjs: "en-gb",
+		alt: true,
+	},
+	pr: {
+		display: "Pirate",
+		emoji: "🏴‍☠️",
+		i18n: "pr",
+		dayjs: "en-gb",
+		alt: true,
+	},
+	bottom: {
+		display: "Bottom",
+		emoji: "🥺",
+		i18n: "bottom",
+		dayjs: "en-gb",
+		alt: true,
+	},
+	piglatin: {
+		display: "Pig Latin",
+		emoji: "🐖",
+		i18n: "piglatin",
+		dayjs: "en-gb",
+		alt: true,
+	},
 };
 
 interface Props {
-    children: JSX.Element | JSX.Element[];
-    locale: Language;
+	children: JSX.Element | JSX.Element[];
+	locale: Language;
 }
 
 function Locale({ children, locale }: Props) {
-    // TODO: create and use LanguageDefinition type here
-    const [defns, setDefinition] = useState<Record<string, unknown>>(definition);
-    const lang = Languages[locale];
-
-    // TODO: clean this up and use the built in Intl API
-    function transformLanguage(source: { [key: string]: any }) {
-        const obj = defaultsDeep(source, definition);
-
-        const dayjs = obj.dayjs;
-        const defaults = dayjs.defaults;
-
-        const twelvehour = defaults?.twelvehour === 'yes' || true;
-        const separator: '/' | '-' | '.' = defaults?.date_separator ?? '/';
-        const date: 'traditional' | 'simplified' | 'ISO8601' = defaults?.date_format ?? 'traditional';
-    
-        const DATE_FORMATS = {
-            traditional: `DD${separator}MM${separator}YYYY`,
-            simplified: `MM${separator}DD${separator}YYYY`,
-            ISO8601: 'YYYY-MM-DD'
-        }
-
-        dayjs['sameElse'] = DATE_FORMATS[date];
-        Object.keys(dayjs)
-            .filter(k => k !== 'defaults')
-            .forEach(k => dayjs[k] = dayjs[k].replace(/{{time}}/g, twelvehour ? 'LT' : 'HH:mm'));
-        
-        return obj;
-    }
-
-    useEffect(() => {
-        if (locale === "en") {
-            const defn = transformLanguage(definition);
-            setDefinition(defn);
-            dayjs.locale("en");
-            dayjs.updateLocale('en', { calendar: defn.dayjs });
-            return;
-        }
-
-        import(`../../external/lang/${lang.i18n}.json`).then(
-            async (lang_file) => {
-                const defn = transformLanguage(lang_file.default);
-                const target = lang.dayjs ?? lang.i18n;
-                const dayjs_locale = await import(`../../node_modules/dayjs/esm/locale/${target}.js`);
-
-                if (defn.dayjs) {
-                    dayjs.updateLocale(target, { calendar: defn.dayjs });
-                }
-
-                dayjs.locale(dayjs_locale.default);
-                setDefinition(defn);
-            }
-        );
-    }, [locale, lang]);
-
-    useEffect(() => {
-        document.body.style.direction = lang.rtl ? "rtl" : "";
-    }, [lang.rtl]);
-
-    return <IntlProvider definition={defns}>{children}</IntlProvider>;
+	// TODO: create and use LanguageDefinition type here
+	const [defns, setDefinition] =
+		useState<Record<string, unknown>>(definition);
+	const lang = Languages[locale];
+
+	// TODO: clean this up and use the built in Intl API
+	function transformLanguage(source: { [key: string]: any }) {
+		const obj = defaultsDeep(source, definition);
+
+		const dayjs = obj.dayjs;
+		const defaults = dayjs.defaults;
+
+		const twelvehour = defaults?.twelvehour === "yes" || true;
+		const separator: "/" | "-" | "." = defaults?.date_separator ?? "/";
+		const date: "traditional" | "simplified" | "ISO8601" =
+			defaults?.date_format ?? "traditional";
+
+		const DATE_FORMATS = {
+			traditional: `DD${separator}MM${separator}YYYY`,
+			simplified: `MM${separator}DD${separator}YYYY`,
+			ISO8601: "YYYY-MM-DD",
+		};
+
+		dayjs["sameElse"] = DATE_FORMATS[date];
+		Object.keys(dayjs)
+			.filter((k) => k !== "defaults")
+			.forEach(
+				(k) =>
+					(dayjs[k] = dayjs[k].replace(
+						/{{time}}/g,
+						twelvehour ? "LT" : "HH:mm",
+					)),
+			);
+
+		return obj;
+	}
+
+	useEffect(() => {
+		if (locale === "en") {
+			const defn = transformLanguage(definition);
+			setDefinition(defn);
+			dayjs.locale("en");
+			dayjs.updateLocale("en", { calendar: defn.dayjs });
+			return;
+		}
+
+		import(`../../external/lang/${lang.i18n}.json`).then(
+			async (lang_file) => {
+				const defn = transformLanguage(lang_file.default);
+				const target = lang.dayjs ?? lang.i18n;
+				const dayjs_locale = await import(
+					`../../node_modules/dayjs/esm/locale/${target}.js`
+				);
+
+				if (defn.dayjs) {
+					dayjs.updateLocale(target, { calendar: defn.dayjs });
+				}
+
+				dayjs.locale(dayjs_locale.default);
+				setDefinition(defn);
+			},
+		);
+	}, [locale, lang]);
+
+	useEffect(() => {
+		document.body.style.direction = lang.rtl ? "rtl" : "";
+	}, [lang.rtl]);
+
+	return <IntlProvider definition={defns}>{children}</IntlProvider>;
 }
 
 export default connectState<Omit<Props, "locale">>(
-    Locale,
-    (state) => {
-        return {
-            locale: state.locale,
-        };
-    },
-    true
+	Locale,
+	(state) => {
+		return {
+			locale: state.locale,
+		};
+	},
+	true,
 );
diff --git a/src/context/Settings.tsx b/src/context/Settings.tsx
index 3653b93e772ada8c62710b82ada757949047e955..587f2a94f2c1b80d0e74ec2069b5ca11dbda81c3 100644
--- a/src/context/Settings.tsx
+++ b/src/context/Settings.tsx
@@ -6,44 +6,56 @@
 // if it does cause problems though.
 //
 // This now also supports Audio stuff.
-
-import { DEFAULT_SOUNDS, Settings, SoundOptions } from "../redux/reducers/settings";
-import { playSound, Sounds } from "../assets/sounds/Audio";
-import { connectState } from "../redux/connector";
 import defaultsDeep from "lodash.defaultsdeep";
-import { Children } from "../types/Preact";
+
 import { createContext } from "preact";
 import { useMemo } from "preact/hooks";
 
+import { connectState } from "../redux/connector";
+import {
+	DEFAULT_SOUNDS,
+	Settings,
+	SoundOptions,
+} from "../redux/reducers/settings";
+
+import { playSound, Sounds } from "../assets/sounds/Audio";
+import { Children } from "../types/Preact";
+
 export const SettingsContext = createContext<Settings>({});
-export const SoundContext = createContext<((sound: Sounds) => void)>(null!);
+export const SoundContext = createContext<(sound: Sounds) => void>(null!);
 
 interface Props {
-    children?: Children,
-    settings: Settings
+	children?: Children;
+	settings: Settings;
 }
 
-function Settings({ settings, children }: Props) {
-    const play = useMemo(() => {
-        const enabled: SoundOptions = defaultsDeep(settings.notification?.sounds ?? {}, DEFAULT_SOUNDS);
-        return (sound: Sounds) => {
-            if (enabled[sound]) {
-                playSound(sound);
-            }
-        };
-    }, [ settings.notification ]);
-
-    return (
-        <SettingsContext.Provider value={settings}>
-            <SoundContext.Provider value={play}>
-                { children }
-            </SoundContext.Provider>
-        </SettingsContext.Provider>
-    )
+function SettingsProvider({ settings, children }: Props) {
+	const play = useMemo(() => {
+		const enabled: SoundOptions = defaultsDeep(
+			settings.notification?.sounds ?? {},
+			DEFAULT_SOUNDS,
+		);
+		return (sound: Sounds) => {
+			if (enabled[sound]) {
+				playSound(sound);
+			}
+		};
+	}, [settings.notification]);
+
+	return (
+		<SettingsContext.Provider value={settings}>
+			<SoundContext.Provider value={play}>
+				{children}
+			</SoundContext.Provider>
+		</SettingsContext.Provider>
+	);
 }
 
-export default connectState<Omit<Props, 'settings'>>(Settings, state => {
-    return {
-        settings: state.settings
-    }
-});
+export default connectState<Omit<Props, "settings">>(
+	SettingsProvider,
+	(state) => {
+		return {
+			settings: state.settings,
+		};
+	},
+);
diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx
index 8a9fc54e11c32954e398f3ab4826726eb64376ef..af0d218abfa1ad41acd84e3923add1647afbfec5 100644
--- a/src/context/Theme.tsx
+++ b/src/context/Theme.tsx
@@ -1,335 +1,360 @@
-import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
+import { Helmet } from "react-helmet";
 import { createGlobalStyle } from "styled-components";
+
+import { createContext } from "preact";
+import { useEffect } from "preact/hooks";
+
+import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
+
 import { connectState } from "../redux/connector";
+
 import { Children } from "../types/Preact";
-import { useEffect } from "preact/hooks";
-import { createContext } from "preact";
-import { Helmet } from "react-helmet";
 
 export type Variables =
-    | "accent"
-    | "background"
-    | "foreground"
-    | "block"
-    | "message-box"
-    | "mention"
-    | "success"
-    | "warning"
-    | "error"
-    | "hover"
-    | "scrollbar-thumb"
-    | "scrollbar-track"
-    | "primary-background"
-    | "primary-header"
-    | "secondary-background"
-    | "secondary-foreground"
-    | "secondary-header"
-    | "tertiary-background"
-    | "tertiary-foreground"
-    | "status-online"
-    | "status-away"
-    | "status-busy"
-    | "status-streaming"
-    | "status-invisible"
+	| "accent"
+	| "background"
+	| "foreground"
+	| "block"
+	| "message-box"
+	| "mention"
+	| "success"
+	| "warning"
+	| "error"
+	| "hover"
+	| "scrollbar-thumb"
+	| "scrollbar-track"
+	| "primary-background"
+	| "primary-header"
+	| "secondary-background"
+	| "secondary-foreground"
+	| "secondary-header"
+	| "tertiary-background"
+	| "tertiary-foreground"
+	| "status-online"
+	| "status-away"
+	| "status-busy"
+	| "status-streaming"
+	| "status-invisible";
 
 // While this isn't used, it'd be good to keep this up to date as a reference or for future use
 export type HiddenVariables =
-    | "font"
-    | "ligatures"
-    | "app-height"
-    | "sidebar-active"
-    | "monospace-font"
+	| "font"
+	| "ligatures"
+	| "app-height"
+	| "sidebar-active"
+	| "monospace-font";
 
-export type Fonts = 'Open Sans' | 'Inter' | 'Atkinson Hyperlegible' | 'Roboto' | 'Noto Sans' | 'Lato' | 'Bree Serif' | 'Montserrat' | 'Poppins' | 'Raleway' | 'Ubuntu' | 'Comic Neue';
-export type MonoscapeFonts = 'Fira Code' | 'Roboto Mono' | 'Source Code Pro' | 'Space Mono' | 'Ubuntu Mono';
+export type Fonts =
+	| "Open Sans"
+	| "Inter"
+	| "Atkinson Hyperlegible"
+	| "Roboto"
+	| "Noto Sans"
+	| "Lato"
+	| "Bree Serif"
+	| "Montserrat"
+	| "Poppins"
+	| "Raleway"
+	| "Ubuntu"
+	| "Comic Neue";
+export type MonoscapeFonts =
+	| "Fira Code"
+	| "Roboto Mono"
+	| "Source Code Pro"
+	| "Space Mono"
+	| "Ubuntu Mono";
 
 export type Theme = {
-    [variable in Variables]: string;
+	[variable in Variables]: string;
 } & {
-    light?: boolean;
-    font?: Fonts;
-    css?: string;
-    monoscapeFont?: MonoscapeFonts;
+	light?: boolean;
+	font?: Fonts;
+	css?: string;
+	monoscapeFont?: MonoscapeFonts;
 };
 
 export interface ThemeOptions {
-    preset?: string;
-    ligatures?: boolean;
-    custom?: Partial<Theme>;
+	preset?: string;
+	ligatures?: boolean;
+	custom?: Partial<Theme>;
 }
 
 // import aaa from "@fontsource/open-sans/300.css?raw";
 // console.info(aaa);
 
-export const FONTS: Record<Fonts, { name: string, load: () => void }> = {
-    "Open Sans": {
-        name: "Open Sans",
-        load: async () => {
-            await import("@fontsource/open-sans/300.css");
-            await import("@fontsource/open-sans/400.css");
-            await import("@fontsource/open-sans/600.css");
-            await import("@fontsource/open-sans/700.css");
-            await import("@fontsource/open-sans/400-italic.css");
-        }
-    },
-    Inter: {
-        name: "Inter",
-        load: async () => {
-            await import("@fontsource/inter/300.css");
-            await import("@fontsource/inter/400.css");
-            await import("@fontsource/inter/600.css");
-            await import("@fontsource/inter/700.css");
-        }
-    },
-    "Atkinson Hyperlegible": {
-        name: "Atkinson Hyperlegible",
-        load: async () => {
-            await import("@fontsource/atkinson-hyperlegible/400.css");
-            await import("@fontsource/atkinson-hyperlegible/700.css");
-            await import("@fontsource/atkinson-hyperlegible/400-italic.css");
-        }
-    },
-    "Roboto": {
-        name: "Roboto",
-        load: async () => {
-            await import("@fontsource/roboto/400.css");
-            await import("@fontsource/roboto/700.css");
-            await import("@fontsource/roboto/400-italic.css");
-        }
-    },
-    "Noto Sans": {
-        name: "Noto Sans",
-        load: async () => {
-            await import("@fontsource/noto-sans/400.css");
-            await import("@fontsource/noto-sans/700.css");
-            await import("@fontsource/noto-sans/400-italic.css");
-        }
-    },
-    "Bree Serif": {
-        name: "Bree Serif",
-        load: () => import("@fontsource/bree-serif/400.css")
-    },
-    "Lato": {
-        name: "Lato",
-        load: async () => {
-            await import("@fontsource/lato/300.css");
-            await import("@fontsource/lato/400.css");
-            await import("@fontsource/lato/700.css");
-            await import("@fontsource/lato/400-italic.css");
-        }
-    },
-    "Montserrat": {
-        name: "Montserrat",
-        load: async () => {
-            await import("@fontsource/montserrat/300.css");
-            await import("@fontsource/montserrat/400.css");
-            await import("@fontsource/montserrat/600.css");
-            await import("@fontsource/montserrat/700.css");
-            await import("@fontsource/montserrat/400-italic.css");
-        }
-    },
-    "Poppins": {
-        name: "Poppins",
-        load: async () => {
-            await import("@fontsource/poppins/300.css");
-            await import("@fontsource/poppins/400.css");
-            await import("@fontsource/poppins/600.css");
-            await import("@fontsource/poppins/700.css");
-            await import("@fontsource/poppins/400-italic.css");
-        }
-    },
-    "Raleway": {
-        name: "Raleway",
-        load: async () => {
-            await import("@fontsource/raleway/300.css");
-            await import("@fontsource/raleway/400.css");
-            await import("@fontsource/raleway/600.css");
-            await import("@fontsource/raleway/700.css");
-            await import("@fontsource/raleway/400-italic.css");
-        }
-    },
-    "Ubuntu": {
-        name: "Ubuntu",
-        load: async () => {
-            await import("@fontsource/ubuntu/300.css");
-            await import("@fontsource/ubuntu/400.css");
-            await import("@fontsource/ubuntu/500.css");
-            await import("@fontsource/ubuntu/700.css");
-            await import("@fontsource/ubuntu/400-italic.css");
-        }
-    },
-    "Comic Neue": {
-        name: "Comic Neue",
-        load: async () => {
-            await import("@fontsource/comic-neue/300.css");
-            await import("@fontsource/comic-neue/400.css");
-            await import("@fontsource/comic-neue/700.css");
-            await import("@fontsource/comic-neue/400-italic.css");
-        }
-    }
+export const FONTS: Record<Fonts, { name: string; load: () => void }> = {
+	"Open Sans": {
+		name: "Open Sans",
+		load: async () => {
+			await import("@fontsource/open-sans/300.css");
+			await import("@fontsource/open-sans/400.css");
+			await import("@fontsource/open-sans/600.css");
+			await import("@fontsource/open-sans/700.css");
+			await import("@fontsource/open-sans/400-italic.css");
+		},
+	},
+	Inter: {
+		name: "Inter",
+		load: async () => {
+			await import("@fontsource/inter/300.css");
+			await import("@fontsource/inter/400.css");
+			await import("@fontsource/inter/600.css");
+			await import("@fontsource/inter/700.css");
+		},
+	},
+	"Atkinson Hyperlegible": {
+		name: "Atkinson Hyperlegible",
+		load: async () => {
+			await import("@fontsource/atkinson-hyperlegible/400.css");
+			await import("@fontsource/atkinson-hyperlegible/700.css");
+			await import("@fontsource/atkinson-hyperlegible/400-italic.css");
+		},
+	},
+	Roboto: {
+		name: "Roboto",
+		load: async () => {
+			await import("@fontsource/roboto/400.css");
+			await import("@fontsource/roboto/700.css");
+			await import("@fontsource/roboto/400-italic.css");
+		},
+	},
+	"Noto Sans": {
+		name: "Noto Sans",
+		load: async () => {
+			await import("@fontsource/noto-sans/400.css");
+			await import("@fontsource/noto-sans/700.css");
+			await import("@fontsource/noto-sans/400-italic.css");
+		},
+	},
+	"Bree Serif": {
+		name: "Bree Serif",
+		load: () => import("@fontsource/bree-serif/400.css"),
+	},
+	Lato: {
+		name: "Lato",
+		load: async () => {
+			await import("@fontsource/lato/300.css");
+			await import("@fontsource/lato/400.css");
+			await import("@fontsource/lato/700.css");
+			await import("@fontsource/lato/400-italic.css");
+		},
+	},
+	Montserrat: {
+		name: "Montserrat",
+		load: async () => {
+			await import("@fontsource/montserrat/300.css");
+			await import("@fontsource/montserrat/400.css");
+			await import("@fontsource/montserrat/600.css");
+			await import("@fontsource/montserrat/700.css");
+			await import("@fontsource/montserrat/400-italic.css");
+		},
+	},
+	Poppins: {
+		name: "Poppins",
+		load: async () => {
+			await import("@fontsource/poppins/300.css");
+			await import("@fontsource/poppins/400.css");
+			await import("@fontsource/poppins/600.css");
+			await import("@fontsource/poppins/700.css");
+			await import("@fontsource/poppins/400-italic.css");
+		},
+	},
+	Raleway: {
+		name: "Raleway",
+		load: async () => {
+			await import("@fontsource/raleway/300.css");
+			await import("@fontsource/raleway/400.css");
+			await import("@fontsource/raleway/600.css");
+			await import("@fontsource/raleway/700.css");
+			await import("@fontsource/raleway/400-italic.css");
+		},
+	},
+	Ubuntu: {
+		name: "Ubuntu",
+		load: async () => {
+			await import("@fontsource/ubuntu/300.css");
+			await import("@fontsource/ubuntu/400.css");
+			await import("@fontsource/ubuntu/500.css");
+			await import("@fontsource/ubuntu/700.css");
+			await import("@fontsource/ubuntu/400-italic.css");
+		},
+	},
+	"Comic Neue": {
+		name: "Comic Neue",
+		load: async () => {
+			await import("@fontsource/comic-neue/300.css");
+			await import("@fontsource/comic-neue/400.css");
+			await import("@fontsource/comic-neue/700.css");
+			await import("@fontsource/comic-neue/400-italic.css");
+		},
+	},
 };
 
-export const MONOSCAPE_FONTS: Record<MonoscapeFonts, { name: string, load: () => void }> = {
-    "Fira Code": {
-        name: "Fira Code",
-        load: () => import("@fontsource/fira-code/400.css")
-    },
-    "Roboto Mono": {
-        name: "Roboto Mono",
-        load: () => import("@fontsource/roboto-mono/400.css")
-    },
-    "Source Code Pro": {
-        name: "Source Code Pro",
-        load: () => import("@fontsource/source-code-pro/400.css")
-    },
-    "Space Mono": {
-        name: "Space Mono",
-        load: () => import("@fontsource/space-mono/400.css")
-    },
-    "Ubuntu Mono": {
-        name: "Ubuntu Mono",
-        load: () => import("@fontsource/ubuntu-mono/400.css")
-    }
+export const MONOSCAPE_FONTS: Record<
+	MonoscapeFonts,
+	{ name: string; load: () => void }
+> = {
+	"Fira Code": {
+		name: "Fira Code",
+		load: () => import("@fontsource/fira-code/400.css"),
+	},
+	"Roboto Mono": {
+		name: "Roboto Mono",
+		load: () => import("@fontsource/roboto-mono/400.css"),
+	},
+	"Source Code Pro": {
+		name: "Source Code Pro",
+		load: () => import("@fontsource/source-code-pro/400.css"),
+	},
+	"Space Mono": {
+		name: "Space Mono",
+		load: () => import("@fontsource/space-mono/400.css"),
+	},
+	"Ubuntu Mono": {
+		name: "Ubuntu Mono",
+		load: () => import("@fontsource/ubuntu-mono/400.css"),
+	},
 };
 
 export const FONT_KEYS = Object.keys(FONTS).sort();
 export const MONOSCAPE_FONT_KEYS = Object.keys(MONOSCAPE_FONTS).sort();
 
-export const DEFAULT_FONT = 'Open Sans';
-export const DEFAULT_MONO_FONT = 'Fira Code';
+export const DEFAULT_FONT = "Open Sans";
+export const DEFAULT_MONO_FONT = "Fira Code";
 
 // Generated from https://gitlab.insrt.uk/revolt/community/themes
 export const PRESETS: Record<string, Theme> = {
-    light: {
-        light: true,
-        accent: "#FD6671",
-        background: "#F6F6F6",
-        foreground: "#101010",
-        block: "#414141",
-        "message-box": "#F1F1F1",
-        mention: "rgba(251, 255, 0, 0.40)",
-        success: "#65E572",
-        warning: "#FAA352",
-        error: "#F06464",
-        hover: "rgba(0, 0, 0, 0.2)",
-        "scrollbar-thumb": "#CA525A",
-        "scrollbar-track": "transparent",
-        "primary-background": "#FFFFFF",
-        "primary-header": "#F1F1F1",
-        "secondary-background": "#F1F1F1",
-        "secondary-foreground": "#888888",
-        "secondary-header": "#F1F1F1",
-        "tertiary-background": "#4D4D4D",
-        "tertiary-foreground": "#646464",
-        "status-online": "#3ABF7E",
-        "status-away": "#F39F00",
-        "status-busy": "#F84848",
-        "status-streaming": "#977EFF",
-        "status-invisible": "#A5A5A5"
-    },
-    dark: {
-        light: false,
-        accent: "#FD6671",
-        background: "#191919",
-        foreground: "#F6F6F6",
-        block: "#2D2D2D",
-        "message-box": "#363636",
-        mention: "rgba(251, 255, 0, 0.06)",
-        success: "#65E572",
-        warning: "#FAA352",
-        error: "#F06464",
-        hover: "rgba(0, 0, 0, 0.1)",
-        "scrollbar-thumb": "#CA525A",
-        "scrollbar-track": "transparent",
-        "primary-background": "#242424",
-        "primary-header": "#363636",
-        "secondary-background": "#1E1E1E",
-        "secondary-foreground": "#C8C8C8",
-        "secondary-header": "#2D2D2D",
-        "tertiary-background": "#4D4D4D",
-        "tertiary-foreground": "#848484",
-        "status-online": "#3ABF7E",
-        "status-away": "#F39F00",
-        "status-busy": "#F84848",
-        "status-streaming": "#977EFF",
-        "status-invisible": "#A5A5A5"
-    },
+	light: {
+		light: true,
+		accent: "#FD6671",
+		background: "#F6F6F6",
+		foreground: "#101010",
+		block: "#414141",
+		"message-box": "#F1F1F1",
+		mention: "rgba(251, 255, 0, 0.40)",
+		success: "#65E572",
+		warning: "#FAA352",
+		error: "#F06464",
+		hover: "rgba(0, 0, 0, 0.2)",
+		"scrollbar-thumb": "#CA525A",
+		"scrollbar-track": "transparent",
+		"primary-background": "#FFFFFF",
+		"primary-header": "#F1F1F1",
+		"secondary-background": "#F1F1F1",
+		"secondary-foreground": "#888888",
+		"secondary-header": "#F1F1F1",
+		"tertiary-background": "#4D4D4D",
+		"tertiary-foreground": "#646464",
+		"status-online": "#3ABF7E",
+		"status-away": "#F39F00",
+		"status-busy": "#F84848",
+		"status-streaming": "#977EFF",
+		"status-invisible": "#A5A5A5",
+	},
+	dark: {
+		light: false,
+		accent: "#FD6671",
+		background: "#191919",
+		foreground: "#F6F6F6",
+		block: "#2D2D2D",
+		"message-box": "#363636",
+		mention: "rgba(251, 255, 0, 0.06)",
+		success: "#65E572",
+		warning: "#FAA352",
+		error: "#F06464",
+		hover: "rgba(0, 0, 0, 0.1)",
+		"scrollbar-thumb": "#CA525A",
+		"scrollbar-track": "transparent",
+		"primary-background": "#242424",
+		"primary-header": "#363636",
+		"secondary-background": "#1E1E1E",
+		"secondary-foreground": "#C8C8C8",
+		"secondary-header": "#2D2D2D",
+		"tertiary-background": "#4D4D4D",
+		"tertiary-foreground": "#848484",
+		"status-online": "#3ABF7E",
+		"status-away": "#F39F00",
+		"status-busy": "#F84848",
+		"status-streaming": "#977EFF",
+		"status-invisible": "#A5A5A5",
+	},
 };
 
 const keys = Object.keys(PRESETS.dark);
 const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
 :root {
 	${(props) =>
-        (Object.keys(props.theme) as Variables[]).map((key) => {
-            if (!keys.includes(key)) return;
-            return `--${key}: ${props.theme[key]};`;
-        })}
+		(Object.keys(props.theme) as Variables[]).map((key) => {
+			if (!keys.includes(key)) return;
+			return `--${key}: ${props.theme[key]};`;
+		})}
 }
 `;
 
 // Load the default default them and apply extras later
-export const ThemeContext = createContext<Theme>(PRESETS['dark']);
+export const ThemeContext = createContext<Theme>(PRESETS["dark"]);
 
 interface Props {
-    children: Children;
-    options?: ThemeOptions;
+	children: Children;
+	options?: ThemeOptions;
 }
 
 function Theme({ children, options }: Props) {
-    const theme: Theme = {
-        ...PRESETS["dark"],
-        ...PRESETS[options?.preset ?? ''],
-        ...options?.custom
-    };
+	const theme: Theme = {
+		...PRESETS["dark"],
+		...PRESETS[options?.preset ?? ""],
+		...options?.custom,
+	};
 
-    const root = document.documentElement.style;
-    useEffect(() => {
-        const font = theme.font ?? DEFAULT_FONT;
-        root.setProperty('--font', `"${font}"`);
-        FONTS[font].load();
-    }, [ theme.font ]);
+	const root = document.documentElement.style;
+	useEffect(() => {
+		const font = theme.font ?? DEFAULT_FONT;
+		root.setProperty("--font", `"${font}"`);
+		FONTS[font].load();
+	}, [theme.font]);
 
-    useEffect(() => {
-        const font = theme.monoscapeFont ?? DEFAULT_MONO_FONT;
-        root.setProperty('--monoscape-font', `"${font}"`);
-        MONOSCAPE_FONTS[font].load();
-    }, [ theme.monoscapeFont ]);
+	useEffect(() => {
+		const font = theme.monoscapeFont ?? DEFAULT_MONO_FONT;
+		root.setProperty("--monoscape-font", `"${font}"`);
+		MONOSCAPE_FONTS[font].load();
+	}, [theme.monoscapeFont]);
 
-    useEffect(() => {
-        root.setProperty('--ligatures', options?.ligatures ? 'normal' : 'none');
-    }, [ options?.ligatures ]);
+	useEffect(() => {
+		root.setProperty("--ligatures", options?.ligatures ? "normal" : "none");
+	}, [options?.ligatures]);
 
-    useEffect(() => {
-        const resize = () => root.setProperty('--app-height', `${window.innerHeight}px`);
-        resize();
+	useEffect(() => {
+		const resize = () =>
+			root.setProperty("--app-height", `${window.innerHeight}px`);
+		resize();
 
-        window.addEventListener('resize', resize);
-        return () => window.removeEventListener('resize', resize);
-    }, []);
+		window.addEventListener("resize", resize);
+		return () => window.removeEventListener("resize", resize);
+	}, []);
 
-    return (
-        <ThemeContext.Provider value={theme}>
-            <Helmet>
-                <meta
-                    name="theme-color"
-                    content={
-                        isTouchscreenDevice
-                            ? theme["primary-header"]
-                            : theme["background"]
-                    }
-                />
-            </Helmet>
-            <GlobalTheme theme={theme} />
-            {theme.css && (
-                <style dangerouslySetInnerHTML={{ __html: theme.css }} />
-            )}
-            { children }
-        </ThemeContext.Provider>
-    );
+	return (
+		<ThemeContext.Provider value={theme}>
+			<Helmet>
+				<meta
+					name="theme-color"
+					content={
+						isTouchscreenDevice
+							? theme["primary-header"]
+							: theme["background"]
+					}
+				/>
+			</Helmet>
+			<GlobalTheme theme={theme} />
+			{theme.css && (
+				<style dangerouslySetInnerHTML={{ __html: theme.css }} />
+			)}
+			{children}
+		</ThemeContext.Provider>
+	);
 }
 
-export default connectState<{ children: Children }>(Theme, state => {
-    return {
-        options: state.settings.theme
-    };
+export default connectState<{ children: Children }>(Theme, (state) => {
+	return {
+		options: state.settings.theme,
+	};
 });
diff --git a/src/context/Voice.tsx b/src/context/Voice.tsx
index 6190c363d31c14909d7032f418fe27ba23b8e05b..b20f5824c790fa84bd82674551fca2476844112c 100644
--- a/src/context/Voice.tsx
+++ b/src/context/Voice.tsx
@@ -1,36 +1,38 @@
 import { createContext } from "preact";
-import { Children } from "../types/Preact";
-import { useForceUpdate } from "./revoltjs/hooks";
-import { AppContext } from "./revoltjs/RevoltClient";
-import type VoiceClient from "../lib/vortex/VoiceClient";
-import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
 import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
+
+import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
+import type VoiceClient from "../lib/vortex/VoiceClient";
+
+import { Children } from "../types/Preact";
 import { SoundContext } from "./Settings";
+import { AppContext } from "./revoltjs/RevoltClient";
+import { useForceUpdate } from "./revoltjs/hooks";
 
 export enum VoiceStatus {
-    LOADING = 0,
-    UNAVAILABLE,
-    ERRORED,
-    READY = 3,
-    CONNECTING = 4,
-    AUTHENTICATING,
-    RTC_CONNECTING,
-    CONNECTED
-    // RECONNECTING
+	LOADING = 0,
+	UNAVAILABLE,
+	ERRORED,
+	READY = 3,
+	CONNECTING = 4,
+	AUTHENTICATING,
+	RTC_CONNECTING,
+	CONNECTED,
+	// RECONNECTING
 }
 
 export interface VoiceOperations {
-    connect: (channelId: string) => Promise<void>;
-    disconnect: () => void;
-    isProducing: (type: ProduceType) => boolean;
-    startProducing: (type: ProduceType) => Promise<void>;
-    stopProducing: (type: ProduceType) => Promise<void> | undefined;
+	connect: (channelId: string) => Promise<void>;
+	disconnect: () => void;
+	isProducing: (type: ProduceType) => boolean;
+	startProducing: (type: ProduceType) => Promise<void>;
+	stopProducing: (type: ProduceType) => Promise<void> | undefined;
 }
 
 export interface VoiceState {
-    roomId?: string;
-    status: VoiceStatus;
-    participants?: Readonly<Map<string, VoiceUser>>;
+	roomId?: string;
+	status: VoiceStatus;
+	participants?: Readonly<Map<string, VoiceUser>>;
 }
 
 // They should be present from first render. - insert's words
@@ -38,166 +40,168 @@ export const VoiceContext = createContext<VoiceState>(null!);
 export const VoiceOperationsContext = createContext<VoiceOperations>(null!);
 
 type Props = {
-    children: Children;
+	children: Children;
 };
 
 export default function Voice({ children }: Props) {
-    const revoltClient = useContext(AppContext);
-    const [client, setClient] = useState<VoiceClient | undefined>(undefined);
-    const [state, setState] = useState<VoiceState>({
-        status: VoiceStatus.LOADING,
-        participants: new Map()
-    });
-
-    function setStatus(status: VoiceStatus, roomId?: string) {
-        setState({
-            status,
-            roomId: roomId ?? client?.roomId,
-            participants: client?.participants ?? new Map(),
-        });
-    }
-
-    useEffect(() => {
-        import('../lib/vortex/VoiceClient')
-            .then(({ default: VoiceClient }) => {
-                const client = new VoiceClient();
-                setClient(client);
-
-                if (!client?.supported()) {
-                    setStatus(VoiceStatus.UNAVAILABLE);
-                } else {
-                    setStatus(VoiceStatus.READY);
-                }
-            })
-            .catch(err => {
-                console.error('Failed to load voice library!', err);
-                setStatus(VoiceStatus.UNAVAILABLE);
-            })
-    }, []);
-
-    const isConnecting = useRef(false);
-    const operations: VoiceOperations = useMemo(() => {
-        return {
-            connect: async channelId => {
-                if (!client?.supported())
-                    throw new Error("RTC is unavailable");
-                
-                isConnecting.current = true;
-                setStatus(VoiceStatus.CONNECTING, channelId);
-
-                try {
-                    const call = await revoltClient.channels.joinCall(
-                        channelId
-                    );
-
-                    if (!isConnecting.current) {
-                        setStatus(VoiceStatus.READY);
-                        return;
-                    }
-
-                    // ! FIXME: use configuration to check if voso is enabled
-                    // await client.connect("wss://voso.revolt.chat/ws");
-                    await client.connect("wss://voso.revolt.chat/ws", channelId);
-
-                    setStatus(VoiceStatus.AUTHENTICATING);
-
-                    await client.authenticate(call.token);
-                    setStatus(VoiceStatus.RTC_CONNECTING);
-
-                    await client.initializeTransports();
-                } catch (error) {
-                    console.error(error);
-                    setStatus(VoiceStatus.READY);
-                    return;
-                }
-
-                setStatus(VoiceStatus.CONNECTED);
-                isConnecting.current = false;
-            },
-            disconnect: () => {
-                if (!client?.supported())
-                    throw new Error("RTC is unavailable");
-            
-                // if (status <= VoiceStatus.READY) return;
-                // this will not update in this context
-
-                isConnecting.current = false;
-                client.disconnect();
-                setStatus(VoiceStatus.READY);
-            },
-            isProducing: (type: ProduceType) => {
-                switch (type) {
-                    case "audio":
-                        return client?.audioProducer !== undefined;
-                }
-            },
-            startProducing: async (type: ProduceType) => {
-                switch (type) {
-                    case "audio": {
-                        if (client?.audioProducer !== undefined) return console.log('No audio producer.'); // ! FIXME: let the user know
-                        if (navigator.mediaDevices === undefined) return console.log('No media devices.'); // ! FIXME: let the user know
-                        const mediaStream = await navigator.mediaDevices.getUserMedia(
-                            {
-                                audio: true
-                            }
-                        );
-
-                        await client?.startProduce(
-                            mediaStream.getAudioTracks()[0],
-                            "audio"
-                        );
-                        return;
-                    }
-                }
-            },
-            stopProducing: (type: ProduceType) => {
-                return client?.stopProduce(type);
-            }
-        }
-    }, [ client ]);
-
-    const { forceUpdate } = useForceUpdate();
-    const playSound = useContext(SoundContext);
-
-    useEffect(() => {
-        if (!client?.supported()) return;
-
-        // ! FIXME: message for fatal:
-        // ! get rid of these force updates
-        // ! handle it through state or smth
-
-        client.on("startProduce",  forceUpdate);
-        client.on("stopProduce", forceUpdate);
-
-        client.on("userJoined", () => {
-            playSound('call_join');
-            forceUpdate();
-        });
-        client.on("userLeft", () => {
-            playSound('call_leave');
-            forceUpdate();
-        });
-        client.on("userStartProduce", forceUpdate);
-        client.on("userStopProduce", forceUpdate);
-        client.on("close", forceUpdate);
-
-        return () => {
-            client.removeListener("startProduce", forceUpdate);
-            client.removeListener("stopProduce", forceUpdate);
-
-            client.removeListener("userJoined", forceUpdate);
-            client.removeListener("userLeft", forceUpdate);
-            client.removeListener("userStartProduce", forceUpdate);
-            client.removeListener("userStopProduce", forceUpdate);
-            client.removeListener("close", forceUpdate);
-        };
-    }, [ client, state ]);
-
-    return (
-        <VoiceContext.Provider value={state}>
-            <VoiceOperationsContext.Provider value={operations}>
-                { children }
-            </VoiceOperationsContext.Provider>
-        </VoiceContext.Provider>
-    );
+	const revoltClient = useContext(AppContext);
+	const [client, setClient] = useState<VoiceClient | undefined>(undefined);
+	const [state, setState] = useState<VoiceState>({
+		status: VoiceStatus.LOADING,
+		participants: new Map(),
+	});
+
+	function setStatus(status: VoiceStatus, roomId?: string) {
+		setState({
+			status,
+			roomId: roomId ?? client?.roomId,
+			participants: client?.participants ?? new Map(),
+		});
+	}
+
+	useEffect(() => {
+		import("../lib/vortex/VoiceClient")
+			.then(({ default: VoiceClient }) => {
+				const client = new VoiceClient();
+				setClient(client);
+
+				if (!client?.supported()) {
+					setStatus(VoiceStatus.UNAVAILABLE);
+				} else {
+					setStatus(VoiceStatus.READY);
+				}
+			})
+			.catch((err) => {
+				console.error("Failed to load voice library!", err);
+				setStatus(VoiceStatus.UNAVAILABLE);
+			});
+	}, []);
+
+	const isConnecting = useRef(false);
+	const operations: VoiceOperations = useMemo(() => {
+		return {
+			connect: async (channelId) => {
+				if (!client?.supported()) throw new Error("RTC is unavailable");
+
+				isConnecting.current = true;
+				setStatus(VoiceStatus.CONNECTING, channelId);
+
+				try {
+					const call = await revoltClient.channels.joinCall(
+						channelId,
+					);
+
+					if (!isConnecting.current) {
+						setStatus(VoiceStatus.READY);
+						return;
+					}
+
+					// ! FIXME: use configuration to check if voso is enabled
+					// await client.connect("wss://voso.revolt.chat/ws");
+					await client.connect(
+						"wss://voso.revolt.chat/ws",
+						channelId,
+					);
+
+					setStatus(VoiceStatus.AUTHENTICATING);
+
+					await client.authenticate(call.token);
+					setStatus(VoiceStatus.RTC_CONNECTING);
+
+					await client.initializeTransports();
+				} catch (error) {
+					console.error(error);
+					setStatus(VoiceStatus.READY);
+					return;
+				}
+
+				setStatus(VoiceStatus.CONNECTED);
+				isConnecting.current = false;
+			},
+			disconnect: () => {
+				if (!client?.supported()) throw new Error("RTC is unavailable");
+
+				// if (status <= VoiceStatus.READY) return;
+				// this will not update in this context
+
+				isConnecting.current = false;
+				client.disconnect();
+				setStatus(VoiceStatus.READY);
+			},
+			isProducing: (type: ProduceType) => {
+				switch (type) {
+					case "audio":
+						return client?.audioProducer !== undefined;
+				}
+			},
+			startProducing: async (type: ProduceType) => {
+				switch (type) {
+					case "audio": {
+						if (client?.audioProducer !== undefined)
+							return console.log("No audio producer."); // ! FIXME: let the user know
+						if (navigator.mediaDevices === undefined)
+							return console.log("No media devices."); // ! FIXME: let the user know
+						const mediaStream =
+							await navigator.mediaDevices.getUserMedia({
+								audio: true,
+							});
+
+						await client?.startProduce(
+							mediaStream.getAudioTracks()[0],
+							"audio",
+						);
+						return;
+					}
+				}
+			},
+			stopProducing: (type: ProduceType) => {
+				return client?.stopProduce(type);
+			},
+		};
+	}, [client]);
+
+	const { forceUpdate } = useForceUpdate();
+	const playSound = useContext(SoundContext);
+
+	useEffect(() => {
+		if (!client?.supported()) return;
+
+		// ! FIXME: message for fatal:
+		// ! get rid of these force updates
+		// ! handle it through state or smth
+
+		client.on("startProduce", forceUpdate);
+		client.on("stopProduce", forceUpdate);
+
+		client.on("userJoined", () => {
+			playSound("call_join");
+			forceUpdate();
+		});
+		client.on("userLeft", () => {
+			playSound("call_leave");
+			forceUpdate();
+		});
+		client.on("userStartProduce", forceUpdate);
+		client.on("userStopProduce", forceUpdate);
+		client.on("close", forceUpdate);
+
+		return () => {
+			client.removeListener("startProduce", forceUpdate);
+			client.removeListener("stopProduce", forceUpdate);
+
+			client.removeListener("userJoined", forceUpdate);
+			client.removeListener("userLeft", forceUpdate);
+			client.removeListener("userStartProduce", forceUpdate);
+			client.removeListener("userStopProduce", forceUpdate);
+			client.removeListener("close", forceUpdate);
+		};
+	}, [client, state]);
+
+	return (
+		<VoiceContext.Provider value={state}>
+			<VoiceOperationsContext.Provider value={operations}>
+				{children}
+			</VoiceOperationsContext.Provider>
+		</VoiceContext.Provider>
+	);
 }
diff --git a/src/context/index.tsx b/src/context/index.tsx
index 4cbbb76bd0f16049a949ef9df491554fa10163f6..f8f94723f59b4317c76bf5caf73511ef7d7480c9 100644
--- a/src/context/index.tsx
+++ b/src/context/index.tsx
@@ -1,32 +1,31 @@
-import State from "../redux/State";
-import { Children } from "../types/Preact";
 import { BrowserRouter as Router } from "react-router-dom";
 
-import Intermediate from './intermediate/Intermediate';
-import Client from './revoltjs/RevoltClient';
-import Settings from "./Settings";
+import State from "../redux/State";
+
+import { Children } from "../types/Preact";
 import Locale from "./Locale";
-import Voice from "./Voice";
+import Settings from "./Settings";
 import Theme from "./Theme";
+import Voice from "./Voice";
+import Intermediate from "./intermediate/Intermediate";
+import Client from "./revoltjs/RevoltClient";
 
 export default function Context({ children }: { children: Children }) {
-    return (
-        <Router>
-            <State>
-                <Theme>
-                    <Settings>
-                        <Locale>
-                            <Intermediate>
-                                <Client>
-                                    <Voice>
-                                        {children}
-                                    </Voice>
-                                </Client>
-                            </Intermediate>
-                        </Locale>
-                    </Settings>
-                </Theme>
-            </State>
-        </Router>
-    );
+	return (
+		<Router>
+			<State>
+				<Theme>
+					<Settings>
+						<Locale>
+							<Intermediate>
+								<Client>
+									<Voice>{children}</Voice>
+								</Client>
+							</Intermediate>
+						</Locale>
+					</Settings>
+				</Theme>
+			</State>
+		</Router>
+	);
 }
diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx
index 0b2929aed192b95d00fa620dad7fd4fc16db196e..8bbdfb1122dd04d891982ccf31c1031a3098777a 100644
--- a/src/context/intermediate/Intermediate.tsx
+++ b/src/context/intermediate/Intermediate.tsx
@@ -1,141 +1,179 @@
-import { Attachment, Channels, EmbedImage, Servers, Users } from "revolt.js/dist/api/objects";
+import { Prompt } from "react-router";
+import { useHistory } from "react-router-dom";
+import {
+	Attachment,
+	Channels,
+	EmbedImage,
+	Servers,
+	Users,
+} from "revolt.js/dist/api/objects";
+
+import { createContext } from "preact";
 import { useContext, useEffect, useMemo, useState } from "preact/hooks";
+
 import { internalSubscribe } from "../../lib/eventEmitter";
+
 import { Action } from "../../components/ui/Modal";
-import { useHistory } from "react-router-dom";
+
 import { Children } from "../../types/Preact";
-import { createContext } from "preact";
-import { Prompt } from "react-router";
-import Modals from './Modals';
+import Modals from "./Modals";
 
 export type Screen =
-| { id: "none" }
-
-// Modals
-| { id: "signed_out" }
-| { id: "error"; error: string }
-| { id: "clipboard"; text: string }
-| { id: "_prompt"; question: Children; content?: Children; actions: Action[] }
-| ({ id: "special_prompt" } & (
-    { type: "leave_group", target: Channels.GroupChannel } |
-    { type: "close_dm", target: Channels.DirectMessageChannel } |
-    { type: "leave_server", target: Servers.Server } |
-    { type: "delete_server", target: Servers.Server } |
-    { type: "delete_channel", target: Channels.TextChannel } |
-    { type: "delete_message", target: Channels.Message } |
-    { type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } |
-    { type: "kick_member", target: Servers.Server, user: string } |
-    { type: "ban_member", target: Servers.Server, user: string } |
-    { type: "unfriend_user", target: Users.User } |
-    { type: "block_user", target: Users.User } |
-    { type: "create_channel", target: Servers.Server }
-)) |
-({ id: "special_input" } & (
-    { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
-    { type: "create_role", server: string, callback: (id: string) => void }
-))
-| {
-      id: "_input";
-      question: Children;
-      field: Children;
-      defaultValue?: string;
-      callback: (value: string) => Promise<void>;
-  }
-| {
-      id: "onboarding";
-      callback: (
-          username: string,
-          loginAfterSuccess?: true
-      ) => Promise<void>;
-  }
-
-// Pop-overs
-| { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage; }
-| { id: "modify_account"; field: "username" | "email" | "password" }
-| { id: "profile"; user_id: string }
-| { id: "channel_info"; channel_id: string }
-| { id: "pending_requests"; users: string[] }
-| {
-      id: "user_picker";
-      omit?: string[];
-      callback: (users: string[]) => Promise<void>;
-  };
+	| { id: "none" }
+
+	// Modals
+	| { id: "signed_out" }
+	| { id: "error"; error: string }
+	| { id: "clipboard"; text: string }
+	| {
+			id: "_prompt";
+			question: Children;
+			content?: Children;
+			actions: Action[];
+	  }
+	| ({ id: "special_prompt" } & (
+			| { type: "leave_group"; target: Channels.GroupChannel }
+			| { type: "close_dm"; target: Channels.DirectMessageChannel }
+			| { type: "leave_server"; target: Servers.Server }
+			| { type: "delete_server"; target: Servers.Server }
+			| { type: "delete_channel"; target: Channels.TextChannel }
+			| { type: "delete_message"; target: Channels.Message }
+			| {
+					type: "create_invite";
+					target: Channels.TextChannel | Channels.GroupChannel;
+			  }
+			| { type: "kick_member"; target: Servers.Server; user: string }
+			| { type: "ban_member"; target: Servers.Server; user: string }
+			| { type: "unfriend_user"; target: Users.User }
+			| { type: "block_user"; target: Users.User }
+			| { type: "create_channel"; target: Servers.Server }
+	  ))
+	| ({ id: "special_input" } & (
+			| {
+					type:
+						| "create_group"
+						| "create_server"
+						| "set_custom_status"
+						| "add_friend";
+			  }
+			| {
+					type: "create_role";
+					server: string;
+					callback: (id: string) => void;
+			  }
+	  ))
+	| {
+			id: "_input";
+			question: Children;
+			field: Children;
+			defaultValue?: string;
+			callback: (value: string) => Promise<void>;
+	  }
+	| {
+			id: "onboarding";
+			callback: (
+				username: string,
+				loginAfterSuccess?: true,
+			) => Promise<void>;
+	  }
+
+	// Pop-overs
+	| { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage }
+	| { id: "modify_account"; field: "username" | "email" | "password" }
+	| { id: "profile"; user_id: string }
+	| { id: "channel_info"; channel_id: string }
+	| { id: "pending_requests"; users: string[] }
+	| {
+			id: "user_picker";
+			omit?: string[];
+			callback: (users: string[]) => Promise<void>;
+	  };
 
 export const IntermediateContext = createContext({
-    screen: { id: "none" } as Screen,
-    focusTaken: false
+	screen: { id: "none" } as Screen,
+	focusTaken: false,
 });
 
 export const IntermediateActionsContext = createContext({
-    openScreen: (screen: Screen) => {},
-    writeClipboard: (text: string) => {}
+	openScreen: (screen: Screen) => {},
+	writeClipboard: (text: string) => {},
 });
 
 interface Props {
-    children: Children;
+	children: Children;
 }
 
 export default function Intermediate(props: Props) {
-    const [screen, openScreen] = useState<Screen>({ id: "none" });
-    const history = useHistory();
-
-    const value = {
-        screen,
-        focusTaken: screen.id !== 'none'
-    };
-
-    const actions = useMemo(() => {
-        return {
-            openScreen: (screen: Screen) => openScreen(screen),
-            writeClipboard: (text: string) => {
-                if (navigator.clipboard) {
-                    navigator.clipboard.writeText(text);
-                } else {
-                    actions.openScreen({ id: "clipboard", text });
-                }
-            }
-        }
-    }, []);
-
-    useEffect(() => {
-        const openProfile = (user_id: string) => openScreen({ id: "profile", user_id });
-        const navigate = (path: string) => history.push(path);
-
-        const subs = [
-            internalSubscribe("Intermediate", "openProfile", openProfile),
-            internalSubscribe("Intermediate", "navigate", navigate)
-        ]
-
-        return () => subs.map(unsub => unsub());
-    }, []);
-
-    return (
-        <IntermediateContext.Provider value={value}>
-            <IntermediateActionsContext.Provider value={actions}>
-                { screen.id !== 'onboarding' && props.children }
-                <Modals
-                    {...value}
-                    {...actions}
-                    key={
-                        screen.id
-                    } /** By specifying a key, we reset state whenever switching screen. */
-                />
-                <Prompt
-                    when={[ 'modify_account', 'special_prompt', 'special_input', 'image_viewer', 'profile', 'channel_info', 'pending_requests', 'user_picker' ].includes(screen.id)}
-                    message={(_, action) => {
-                        if (action === 'POP') {
-                            openScreen({ id: 'none' });
-                            setTimeout(() => history.push(history.location), 0);
-
-                            return false;
-                        }
-
-                        return true;
-                    }}
-                />
-            </IntermediateActionsContext.Provider>
-        </IntermediateContext.Provider>
-    );
+	const [screen, openScreen] = useState<Screen>({ id: "none" });
+	const history = useHistory();
+
+	const value = {
+		screen,
+		focusTaken: screen.id !== "none",
+	};
+
+	const actions = useMemo(() => {
+		return {
+			openScreen: (screen: Screen) => openScreen(screen),
+			writeClipboard: (text: string) => {
+				if (navigator.clipboard) {
+					navigator.clipboard.writeText(text);
+				} else {
+					actions.openScreen({ id: "clipboard", text });
+				}
+			},
+		};
+	}, []);
+
+	useEffect(() => {
+		const openProfile = (user_id: string) =>
+			openScreen({ id: "profile", user_id });
+		const navigate = (path: string) => history.push(path);
+
+		const subs = [
+			internalSubscribe("Intermediate", "openProfile", openProfile),
+			internalSubscribe("Intermediate", "navigate", navigate),
+		];
+
+		return () => subs.map((unsub) => unsub());
+	}, []);
+
+	return (
+		<IntermediateContext.Provider value={value}>
+			<IntermediateActionsContext.Provider value={actions}>
+				{screen.id !== "onboarding" && props.children}
+				<Modals
+					{...value}
+					{...actions}
+					key={
+						screen.id
+					} /** By specifying a key, we reset state whenever switching screen. */
+				/>
+				<Prompt
+					when={[
+						"modify_account",
+						"special_prompt",
+						"special_input",
+						"image_viewer",
+						"profile",
+						"channel_info",
+						"pending_requests",
+						"user_picker",
+					].includes(screen.id)}
+					message={(_, action) => {
+						if (action === "POP") {
+							openScreen({ id: "none" });
+							setTimeout(() => history.push(history.location), 0);
+
+							return false;
+						}
+
+						return true;
+					}}
+				/>
+			</IntermediateActionsContext.Provider>
+		</IntermediateContext.Provider>
+	);
 }
 
 export const useIntermediate = () => useContext(IntermediateActionsContext);
diff --git a/src/context/intermediate/Modals.tsx b/src/context/intermediate/Modals.tsx
index e825bcbc998cfbf4770450546efe02f2f38fd14d..5ca0c164d7a45ffa99e5f7954cd4d6fe8bc214a8 100644
--- a/src/context/intermediate/Modals.tsx
+++ b/src/context/intermediate/Modals.tsx
@@ -1,34 +1,33 @@
 import { Screen } from "./Intermediate";
-
+import { ClipboardModal } from "./modals/Clipboard";
 import { ErrorModal } from "./modals/Error";
 import { InputModal } from "./modals/Input";
+import { OnboardingModal } from "./modals/Onboarding";
 import { PromptModal } from "./modals/Prompt";
 import { SignedOutModal } from "./modals/SignedOut";
-import { ClipboardModal } from "./modals/Clipboard";
-import { OnboardingModal } from "./modals/Onboarding";
 
 export interface Props {
-    screen: Screen;
-    openScreen: (id: any) => void;
+	screen: Screen;
+	openScreen: (id: any) => void;
 }
 
 export default function Modals({ screen, openScreen }: Props) {
-    const onClose = () => openScreen({ id: "none" });
+	const onClose = () => openScreen({ id: "none" });
 
-    switch (screen.id) {
-        case "_prompt":
-            return <PromptModal onClose={onClose} {...screen} />;
-        case "_input":
-            return <InputModal onClose={onClose} {...screen} />;
-        case "error":
-            return <ErrorModal onClose={onClose} {...screen} />;
-        case "signed_out":
-            return <SignedOutModal onClose={onClose} {...screen} />;
-        case "clipboard":
-            return <ClipboardModal onClose={onClose} {...screen} />;
-        case "onboarding":
-            return <OnboardingModal onClose={onClose} {...screen} />;
-    }
+	switch (screen.id) {
+		case "_prompt":
+			return <PromptModal onClose={onClose} {...screen} />;
+		case "_input":
+			return <InputModal onClose={onClose} {...screen} />;
+		case "error":
+			return <ErrorModal onClose={onClose} {...screen} />;
+		case "signed_out":
+			return <SignedOutModal onClose={onClose} {...screen} />;
+		case "clipboard":
+			return <ClipboardModal onClose={onClose} {...screen} />;
+		case "onboarding":
+			return <OnboardingModal onClose={onClose} {...screen} />;
+	}
 
-    return null;
+	return null;
 }
diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx
index d0ee430584f150825c1e7860776ae46c0112b08e..f171f9f0851c428daa83ac5b2843e5c1e3ca5eb3 100644
--- a/src/context/intermediate/Popovers.tsx
+++ b/src/context/intermediate/Popovers.tsx
@@ -1,39 +1,39 @@
-import { IntermediateContext, useIntermediate } from "./Intermediate";
 import { useContext } from "preact/hooks";
 
-import { UserPicker } from "./popovers/UserPicker";
+import { IntermediateContext, useIntermediate } from "./Intermediate";
 import { SpecialInputModal } from "./modals/Input";
 import { SpecialPromptModal } from "./modals/Prompt";
-import { UserProfile } from "./popovers/UserProfile";
-import { ImageViewer } from "./popovers/ImageViewer";
 import { ChannelInfo } from "./popovers/ChannelInfo";
-import { PendingRequests } from "./popovers/PendingRequests";
+import { ImageViewer } from "./popovers/ImageViewer";
 import { ModifyAccountModal } from "./popovers/ModifyAccount";
+import { PendingRequests } from "./popovers/PendingRequests";
+import { UserPicker } from "./popovers/UserPicker";
+import { UserProfile } from "./popovers/UserProfile";
 
 export default function Popovers() {
-    const { screen } = useContext(IntermediateContext);
-    const { openScreen } = useIntermediate();
+	const { screen } = useContext(IntermediateContext);
+	const { openScreen } = useIntermediate();
 
-    const onClose = () => openScreen({ id: "none" });
+	const onClose = () => openScreen({ id: "none" });
 
-    switch (screen.id) {
-        case "profile":
-            return <UserProfile {...screen} onClose={onClose} />;
-        case "user_picker":
-            return <UserPicker {...screen} onClose={onClose} />;
-        case "image_viewer":
-            return <ImageViewer {...screen} onClose={onClose} />;
-        case "channel_info":
-            return <ChannelInfo {...screen} onClose={onClose} />;
-        case "pending_requests":
-            return <PendingRequests {...screen} onClose={onClose} />;
-        case "modify_account":
-            return <ModifyAccountModal onClose={onClose} {...screen} />;
-        case "special_prompt":
-            return <SpecialPromptModal onClose={onClose} {...screen} />;
-        case "special_input":
-            return <SpecialInputModal onClose={onClose} {...screen} />;
-    }
+	switch (screen.id) {
+		case "profile":
+			return <UserProfile {...screen} onClose={onClose} />;
+		case "user_picker":
+			return <UserPicker {...screen} onClose={onClose} />;
+		case "image_viewer":
+			return <ImageViewer {...screen} onClose={onClose} />;
+		case "channel_info":
+			return <ChannelInfo {...screen} onClose={onClose} />;
+		case "pending_requests":
+			return <PendingRequests {...screen} onClose={onClose} />;
+		case "modify_account":
+			return <ModifyAccountModal onClose={onClose} {...screen} />;
+		case "special_prompt":
+			return <SpecialPromptModal onClose={onClose} {...screen} />;
+		case "special_input":
+			return <SpecialInputModal onClose={onClose} {...screen} />;
+	}
 
-    return null;
+	return null;
 }
diff --git a/src/context/intermediate/modals/Clipboard.tsx b/src/context/intermediate/modals/Clipboard.tsx
index f0b12badba5d4d606083752f30611fdf607bf52c..8b238373ea0308cb8ff89abf2d0ae5e95dcf6a2f 100644
--- a/src/context/intermediate/modals/Clipboard.tsx
+++ b/src/context/intermediate/modals/Clipboard.tsx
@@ -1,32 +1,32 @@
 import { Text } from "preact-i18n";
+
 import Modal from "../../../components/ui/Modal";
 
 interface Props {
-    onClose: () => void;
-    text: string;
+	onClose: () => void;
+	text: string;
 }
 
 export function ClipboardModal({ onClose, text }: Props) {
-    return (
-        <Modal
-            visible={true}
-            onClose={onClose}
-            title={<Text id="app.special.modals.clipboard.unavailable" />}
-            actions={[
-                {
-                    onClick: onClose,
-                    confirmation: true,
-                    text: <Text id="app.special.modals.actions.close" />
-                }
-            ]}
-        >
-            {location.protocol !== "https:" && (
-                <p>
-                    <Text id="app.special.modals.clipboard.https" />
-                </p>
-            )}
-            <Text id="app.special.modals.clipboard.copy" />{" "}
-            <code style={{ userSelect: "all" }}>{text}</code>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible={true}
+			onClose={onClose}
+			title={<Text id="app.special.modals.clipboard.unavailable" />}
+			actions={[
+				{
+					onClick: onClose,
+					confirmation: true,
+					text: <Text id="app.special.modals.actions.close" />,
+				},
+			]}>
+			{location.protocol !== "https:" && (
+				<p>
+					<Text id="app.special.modals.clipboard.https" />
+				</p>
+			)}
+			<Text id="app.special.modals.clipboard.copy" />{" "}
+			<code style={{ userSelect: "all" }}>{text}</code>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/modals/Error.tsx b/src/context/intermediate/modals/Error.tsx
index 5b3cc1707461fbbe9c84653863738325c6c34980..81d115255ccaf77ead5f4eed211d26df342990a6 100644
--- a/src/context/intermediate/modals/Error.tsx
+++ b/src/context/intermediate/modals/Error.tsx
@@ -1,30 +1,30 @@
 import { Text } from "preact-i18n";
+
 import Modal from "../../../components/ui/Modal";
 
 interface Props {
-    onClose: () => void;
-    error: string;
+	onClose: () => void;
+	error: string;
 }
 
 export function ErrorModal({ onClose, error }: Props) {
-    return (
-        <Modal
-            visible={true}
-            onClose={() => false}
-            title={<Text id="app.special.modals.error" />}
-            actions={[
-                {
-                    onClick: onClose,
-                    confirmation: true,
-                    text: <Text id="app.special.modals.actions.ok" />
-                },
-                {
-                    onClick: () => location.reload(),
-                    text: <Text id="app.special.modals.actions.reload" />
-                }
-            ]}
-        >
-            <Text id={`error.${error}`}>{error}</Text>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible={true}
+			onClose={() => false}
+			title={<Text id="app.special.modals.error" />}
+			actions={[
+				{
+					onClick: onClose,
+					confirmation: true,
+					text: <Text id="app.special.modals.actions.ok" />,
+				},
+				{
+					onClick: () => location.reload(),
+					text: <Text id="app.special.modals.actions.reload" />,
+				},
+			]}>
+			<Text id={`error.${error}`}>{error}</Text>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx
index d57ba8d81539a2a80e4e63217b5b0e572bc49e64..36e6026d4f1c35dbac3ea01e7db2129845d42099 100644
--- a/src/context/intermediate/modals/Input.tsx
+++ b/src/context/intermediate/modals/Input.tsx
@@ -1,154 +1,176 @@
+import { useHistory } from "react-router";
 import { ulid } from "ulid";
+
 import { Text } from "preact-i18n";
-import { useHistory } from "react-router";
+import { useContext, useState } from "preact/hooks";
+
+import InputBox from "../../../components/ui/InputBox";
 import Modal from "../../../components/ui/Modal";
+import Overline from "../../../components/ui/Overline";
+
 import { Children } from "../../../types/Preact";
-import { takeError } from "../../revoltjs/util";
-import { useContext, useState } from "preact/hooks";
-import Overline from '../../../components/ui/Overline';
-import InputBox from '../../../components/ui/InputBox';
 import { AppContext } from "../../revoltjs/RevoltClient";
+import { takeError } from "../../revoltjs/util";
 
 interface Props {
-    onClose: () => void;
-    question: Children;
-    field?: Children;
-    defaultValue?: string;
-    callback: (value: string) => Promise<void>;
+	onClose: () => void;
+	question: Children;
+	field?: Children;
+	defaultValue?: string;
+	callback: (value: string) => Promise<void>;
 }
 
 export function InputModal({
-    onClose,
-    question,
-    field,
-    defaultValue,
-    callback
+	onClose,
+	question,
+	field,
+	defaultValue,
+	callback,
 }: Props) {
-    const [processing, setProcessing] = useState(false);
-    const [value, setValue] = useState(defaultValue ?? "");
-    const [error, setError] = useState<undefined | string>(undefined);
+	const [processing, setProcessing] = useState(false);
+	const [value, setValue] = useState(defaultValue ?? "");
+	const [error, setError] = useState<undefined | string>(undefined);
 
-    return (
-        <Modal
-            visible={true}
-            title={question}
-            disabled={processing}
-            actions={[
-                {
-                    confirmation: true,
-                    text: <Text id="app.special.modals.actions.ok" />,
-                    onClick: () => {
-                        setProcessing(true);
-                        callback(value)
-                            .then(onClose)
-                            .catch(err => {
-                                setError(takeError(err));
-                                setProcessing(false)
-                            })
-                    }
-                },
-                {
-                    text: <Text id="app.special.modals.actions.cancel" />,
-                    onClick: onClose
-                }
-            ]}
-            onClose={onClose}
-        >
-            <form>
-                { field ? <Overline error={error} block>
-                    {field}
-                </Overline> : (error && <Overline error={error} type="error" block />) }
-                <InputBox
-                    value={value}
-                    onChange={e => setValue(e.currentTarget.value)}
-                />
-            </form>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible={true}
+			title={question}
+			disabled={processing}
+			actions={[
+				{
+					confirmation: true,
+					text: <Text id="app.special.modals.actions.ok" />,
+					onClick: () => {
+						setProcessing(true);
+						callback(value)
+							.then(onClose)
+							.catch((err) => {
+								setError(takeError(err));
+								setProcessing(false);
+							});
+					},
+				},
+				{
+					text: <Text id="app.special.modals.actions.cancel" />,
+					onClick: onClose,
+				},
+			]}
+			onClose={onClose}>
+			<form>
+				{field ? (
+					<Overline error={error} block>
+						{field}
+					</Overline>
+				) : (
+					error && <Overline error={error} type="error" block />
+				)}
+				<InputBox
+					value={value}
+					onChange={(e) => setValue(e.currentTarget.value)}
+				/>
+			</form>
+		</Modal>
+	);
 }
 
 type SpecialProps = { onClose: () => void } & (
-    { type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
-    { type: "create_role", server: string, callback: (id: string) => void }
-)
+	| {
+			type:
+				| "create_group"
+				| "create_server"
+				| "set_custom_status"
+				| "add_friend";
+	  }
+	| { type: "create_role"; server: string; callback: (id: string) => void }
+);
 
 export function SpecialInputModal(props: SpecialProps) {
-    const history = useHistory();
-    const client = useContext(AppContext);
+	const history = useHistory();
+	const client = useContext(AppContext);
 
-    const { onClose } = props;
-    switch (props.type) {
-        case "create_group": {
-            return <InputModal
-                onClose={onClose}
-                question={<Text id="app.main.groups.create" />}
-                field={<Text id="app.main.groups.name" />}
-                callback={async name => {
-                    const group = await client.channels.createGroup(
-                        {
-                            name,
-                            nonce: ulid(),
-                            users: []
-                        }
-                    );
+	const { onClose } = props;
+	switch (props.type) {
+		case "create_group": {
+			return (
+				<InputModal
+					onClose={onClose}
+					question={<Text id="app.main.groups.create" />}
+					field={<Text id="app.main.groups.name" />}
+					callback={async (name) => {
+						const group = await client.channels.createGroup({
+							name,
+							nonce: ulid(),
+							users: [],
+						});
 
-                    history.push(`/channel/${group._id}`);
-                }}
-            />;
-        }
-        case "create_server": {
-            return <InputModal
-                onClose={onClose}
-                question={<Text id="app.main.servers.create" />}
-                field={<Text id="app.main.servers.name" />}
-                callback={async name => {
-                    const server = await client.servers.createServer(
-                        {
-                            name,
-                            nonce: ulid()
-                        }
-                    );
+						history.push(`/channel/${group._id}`);
+					}}
+				/>
+			);
+		}
+		case "create_server": {
+			return (
+				<InputModal
+					onClose={onClose}
+					question={<Text id="app.main.servers.create" />}
+					field={<Text id="app.main.servers.name" />}
+					callback={async (name) => {
+						const server = await client.servers.createServer({
+							name,
+							nonce: ulid(),
+						});
 
-                    history.push(`/server/${server._id}`);
-                }}
-            />;
-        }
-        case "create_role": {
-            return <InputModal
-                onClose={onClose}
-                question={<Text id="app.settings.permissions.create_role" />}
-                field={<Text id="app.settings.permissions.role_name" />}
-                callback={async name => {
-                    const role = await client.servers.createRole(props.server, name);
-                    props.callback(role.id);
-                }}
-            />;
-        }
-        case "set_custom_status": {
-            return <InputModal
-                onClose={onClose}
-                question={<Text id="app.context_menu.set_custom_status" />}
-                field={<Text id="app.context_menu.custom_status" />}
-                defaultValue={client.user?.status?.text}
-                callback={text =>
-                    client.users.editUser({
-                        status: {
-                            ...client.user?.status,
-                            text: text.trim().length > 0 ? text : undefined
-                        }
-                    })
-                }
-            />;
-        }
-        case "add_friend": {
-            return <InputModal
-                onClose={onClose}
-                question={"Add Friend"}
-                callback={username =>
-                    client.users.addFriend(username)
-                }
-            />;
-        }
-        default: return null;
-    }
+						history.push(`/server/${server._id}`);
+					}}
+				/>
+			);
+		}
+		case "create_role": {
+			return (
+				<InputModal
+					onClose={onClose}
+					question={
+						<Text id="app.settings.permissions.create_role" />
+					}
+					field={<Text id="app.settings.permissions.role_name" />}
+					callback={async (name) => {
+						const role = await client.servers.createRole(
+							props.server,
+							name,
+						);
+						props.callback(role.id);
+					}}
+				/>
+			);
+		}
+		case "set_custom_status": {
+			return (
+				<InputModal
+					onClose={onClose}
+					question={<Text id="app.context_menu.set_custom_status" />}
+					field={<Text id="app.context_menu.custom_status" />}
+					defaultValue={client.user?.status?.text}
+					callback={(text) =>
+						client.users.editUser({
+							status: {
+								...client.user?.status,
+								text: text.trim().length > 0 ? text : undefined,
+							},
+						})
+					}
+				/>
+			);
+		}
+		case "add_friend": {
+			return (
+				<InputModal
+					onClose={onClose}
+					question={"Add Friend"}
+					callback={(username) => client.users.addFriend(username)}
+				/>
+			);
+		}
+		default:
+			return null;
+	}
 }
diff --git a/src/context/intermediate/modals/Onboarding.tsx b/src/context/intermediate/modals/Onboarding.tsx
index 3f328191a9f8bbb804cecdef303f6569c9b428b5..35934bbe0991c02e464fc239fd548d7e8b1fe4ff 100644
--- a/src/context/intermediate/modals/Onboarding.tsx
+++ b/src/context/intermediate/modals/Onboarding.tsx
@@ -1,71 +1,78 @@
-import { Text } from "preact-i18n";
-import { useState } from "preact/hooks";
 import { SubmitHandler, useForm } from "react-hook-form";
+
 import styles from "./Onboarding.module.scss";
-import { takeError } from "../../revoltjs/util";
+import { Text } from "preact-i18n";
+import { useState } from "preact/hooks";
+
+import wideSVG from "../../../assets/wide.svg";
 import Button from "../../../components/ui/Button";
-import FormField from "../../../pages/login/FormField";
 import Preloader from "../../../components/ui/Preloader";
 
-import wideSVG from '../../../assets/wide.svg';
+import FormField from "../../../pages/login/FormField";
+import { takeError } from "../../revoltjs/util";
 
 interface Props {
-    onClose: () => void;
-    callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
+	onClose: () => void;
+	callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
 }
 
 interface FormInputs {
-    username: string
+	username: string;
 }
 
 export function OnboardingModal({ onClose, callback }: Props) {
-    const { handleSubmit, register } = useForm<FormInputs>();
-    const [loading, setLoading] = useState(false);
-    const [error, setError] = useState<string | undefined>(undefined);
+	const { handleSubmit, register } = useForm<FormInputs>();
+	const [loading, setLoading] = useState(false);
+	const [error, setError] = useState<string | undefined>(undefined);
 
-    const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
-        setLoading(true);
-        callback(username, true)
-            .then(onClose)
-            .catch((err: any) => {
-                setError(takeError(err));
-                setLoading(false);
-            });
-    }
+	const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
+		setLoading(true);
+		callback(username, true)
+			.then(onClose)
+			.catch((err: any) => {
+				setError(takeError(err));
+				setLoading(false);
+			});
+	};
 
-    return (
-        <div className={styles.onboarding}>
-            <div className={styles.header}>
-                <h1>
-                    <Text id="app.special.modals.onboarding.welcome" />
-                    <img src={wideSVG} />
-                </h1>
-            </div>
-            <div className={styles.form}>
-                {loading ? (
-                    <Preloader type="spinner" />
-                ) : (
-                    <>
-                        <p>
-                            <Text id="app.special.modals.onboarding.pick" />
-                        </p>
-                        <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
-                            <div>
-                                <FormField
-                                    type="username"
-                                    register={register}
-                                    showOverline
-                                    error={error}
-                                />
-                            </div>
-                            <Button type="submit">
-                                <Text id="app.special.modals.actions.continue" />
-                            </Button>
-                        </form>
-                    </>
-                )}
-            </div>
-            <div />
-        </div>
-    );
+	return (
+		<div className={styles.onboarding}>
+			<div className={styles.header}>
+				<h1>
+					<Text id="app.special.modals.onboarding.welcome" />
+					<img src={wideSVG} />
+				</h1>
+			</div>
+			<div className={styles.form}>
+				{loading ? (
+					<Preloader type="spinner" />
+				) : (
+					<>
+						<p>
+							<Text id="app.special.modals.onboarding.pick" />
+						</p>
+						<form
+							onSubmit={
+								handleSubmit(
+									onSubmit,
+								) as JSX.GenericEventHandler<HTMLFormElement>
+							}>
+							<div>
+								<FormField
+									type="username"
+									register={register}
+									showOverline
+									error={error}
+								/>
+							</div>
+							<Button type="submit">
+								<Text id="app.special.modals.actions.continue" />
+							</Button>
+						</form>
+					</>
+				)}
+			</div>
+			<div />
+		</div>
+	);
 }
diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx
index 1af00210afa6ff99c636ea4141419060ad9c9e28..dd21a9ce0f0a00627313a33bf560ba6c4c5e6777 100644
--- a/src/context/intermediate/modals/Prompt.tsx
+++ b/src/context/intermediate/modals/Prompt.tsx
@@ -1,346 +1,473 @@
+import { useHistory } from "react-router-dom";
+import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
 import { ulid } from "ulid";
+
+import styles from "./Prompt.module.scss";
 import { Text } from "preact-i18n";
-import styles from './Prompt.module.scss';
-import { useHistory } from "react-router-dom";
-import Radio from "../../../components/ui/Radio";
-import { Children } from "../../../types/Preact";
-import { useIntermediate } from "../Intermediate";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+import { TextReact } from "../../../lib/i18n";
+
+import Message from "../../../components/common/messaging/Message";
+import UserIcon from "../../../components/common/user/UserIcon";
 import InputBox from "../../../components/ui/InputBox";
+import Modal, { Action } from "../../../components/ui/Modal";
 import Overline from "../../../components/ui/Overline";
+import Radio from "../../../components/ui/Radio";
+
+import { Children } from "../../../types/Preact";
 import { AppContext } from "../../revoltjs/RevoltClient";
 import { mapMessage, takeError } from "../../revoltjs/util";
-import Modal, { Action } from "../../../components/ui/Modal";
-import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
-import { useContext, useEffect, useState } from "preact/hooks";
-import UserIcon from "../../../components/common/user/UserIcon";
-import Message from "../../../components/common/messaging/Message";
-import { TextReact } from "../../../lib/i18n";
+import { useIntermediate } from "../Intermediate";
 
 interface Props {
-    onClose: () => void;
-    question: Children;
-    content?: Children;
-    disabled?: boolean;
-    actions: Action[];
-    error?: string;
+	onClose: () => void;
+	question: Children;
+	content?: Children;
+	disabled?: boolean;
+	actions: Action[];
+	error?: string;
 }
 
-export function PromptModal({ onClose, question, content, actions, disabled, error }: Props) {
-    return (
-        <Modal
-            visible={true}
-            title={question}
-            actions={actions}
-            onClose={onClose}
-            disabled={disabled}>
-            { error && <Overline error={error} type="error" /> }
-            { content }
-        </Modal>
-    );
+export function PromptModal({
+	onClose,
+	question,
+	content,
+	actions,
+	disabled,
+	error,
+}: Props) {
+	return (
+		<Modal
+			visible={true}
+			title={question}
+			actions={actions}
+			onClose={onClose}
+			disabled={disabled}>
+			{error && <Overline error={error} type="error" />}
+			{content}
+		</Modal>
+	);
 }
 
 type SpecialProps = { onClose: () => void } & (
-    { type: "leave_group", target: Channels.GroupChannel } |
-    { type: "close_dm", target: Channels.DirectMessageChannel } |
-    { type: "leave_server", target: Servers.Server } |
-    { type: "delete_server", target: Servers.Server } |
-    { type: "delete_channel", target: Channels.TextChannel } |
-    { type: "delete_message", target: Channels.Message } |
-    { type: "create_invite", target: Channels.TextChannel | Channels.GroupChannel } |
-    { type: "kick_member", target: Servers.Server, user: string } |
-    { type: "ban_member", target: Servers.Server, user: string } |
-    { type: "unfriend_user", target: Users.User } |
-    { type: "block_user", target: Users.User } |
-    { type: "create_channel", target: Servers.Server }
-)
+	| { type: "leave_group"; target: Channels.GroupChannel }
+	| { type: "close_dm"; target: Channels.DirectMessageChannel }
+	| { type: "leave_server"; target: Servers.Server }
+	| { type: "delete_server"; target: Servers.Server }
+	| { type: "delete_channel"; target: Channels.TextChannel }
+	| { type: "delete_message"; target: Channels.Message }
+	| {
+			type: "create_invite";
+			target: Channels.TextChannel | Channels.GroupChannel;
+	  }
+	| { type: "kick_member"; target: Servers.Server; user: string }
+	| { type: "ban_member"; target: Servers.Server; user: string }
+	| { type: "unfriend_user"; target: Users.User }
+	| { type: "block_user"; target: Users.User }
+	| { type: "create_channel"; target: Servers.Server }
+);
 
 export function SpecialPromptModal(props: SpecialProps) {
-    const client = useContext(AppContext);
-    const [ processing, setProcessing ] = useState(false);
-    const [ error, setError ] = useState<undefined | string>(undefined);
+	const client = useContext(AppContext);
+	const [processing, setProcessing] = useState(false);
+	const [error, setError] = useState<undefined | string>(undefined);
+
+	const { onClose } = props;
+	switch (props.type) {
+		case "leave_group":
+		case "close_dm":
+		case "leave_server":
+		case "delete_server":
+		case "delete_channel":
+		case "unfriend_user":
+		case "block_user": {
+			const EVENTS = {
+				close_dm: ["confirm_close_dm", "close"],
+				delete_server: ["confirm_delete", "delete"],
+				delete_channel: ["confirm_delete", "delete"],
+				leave_group: ["confirm_leave", "leave"],
+				leave_server: ["confirm_leave", "leave"],
+				unfriend_user: ["unfriend_user", "remove"],
+				block_user: ["block_user", "block"],
+			};
+
+			let event = EVENTS[props.type];
+			let name;
+			switch (props.type) {
+				case "unfriend_user":
+				case "block_user":
+					name = props.target.username;
+					break;
+				case "close_dm":
+					name = client.users.get(
+						client.channels.getRecipient(props.target._id),
+					)?.username;
+					break;
+				default:
+					name = props.target.name;
+			}
+
+			return (
+				<PromptModal
+					onClose={onClose}
+					question={
+						<Text
+							id={`app.special.modals.prompt.${event[0]}`}
+							fields={{ name }}
+						/>
+					}
+					actions={[
+						{
+							confirmation: true,
+							contrast: true,
+							error: true,
+							text: (
+								<Text
+									id={`app.special.modals.actions.${event[1]}`}
+								/>
+							),
+							onClick: async () => {
+								setProcessing(true);
 
-    const { onClose } = props;
-    switch (props.type) {
-        case 'leave_group':
-        case 'close_dm':
-        case 'leave_server':
-        case 'delete_server': 
-        case 'delete_channel':
-        case 'unfriend_user': 
-        case 'block_user': {
-            const EVENTS = {
-                'close_dm':       ['confirm_close_dm', 'close'],
-                'delete_server':  ['confirm_delete', 'delete'],
-                'delete_channel': ['confirm_delete', 'delete'],
-                'leave_group':    ['confirm_leave', 'leave'],
-                'leave_server':   ['confirm_leave', 'leave'],
-                'unfriend_user':  ['unfriend_user', 'remove'],
-                'block_user':     ['block_user', 'block']
-            };
+								try {
+									switch (props.type) {
+										case "unfriend_user":
+											await client.users.removeFriend(
+												props.target._id,
+											);
+											break;
+										case "block_user":
+											await client.users.blockUser(
+												props.target._id,
+											);
+											break;
+										case "leave_group":
+										case "close_dm":
+										case "delete_channel":
+											await client.channels.delete(
+												props.target._id,
+											);
+											break;
+										case "leave_server":
+										case "delete_server":
+											await client.servers.delete(
+												props.target._id,
+											);
+											break;
+									}
 
-            let event = EVENTS[props.type];
-            let name;
-            switch (props.type) {
-                case 'unfriend_user':
-                case 'block_user': name = props.target.username; break;
-                case 'close_dm': name = client.users.get(client.channels.getRecipient(props.target._id))?.username; break;
-                default: name = props.target.name;
-            }
+									onClose();
+								} catch (err) {
+									setError(takeError(err));
+									setProcessing(false);
+								}
+							},
+						},
+						{
+							text: (
+								<Text id="app.special.modals.actions.cancel" />
+							),
+							onClick: onClose,
+						},
+					]}
+					content={
+						<TextReact
+							id={`app.special.modals.prompt.${event[0]}_long`}
+							fields={{ name: <b>{name}</b> }}
+						/>
+					}
+					disabled={processing}
+					error={error}
+				/>
+			);
+		}
+		case "delete_message": {
+			return (
+				<PromptModal
+					onClose={onClose}
+					question={<Text id={"app.context_menu.delete_message"} />}
+					actions={[
+						{
+							confirmation: true,
+							contrast: true,
+							error: true,
+							text: (
+								<Text id="app.special.modals.actions.delete" />
+							),
+							onClick: async () => {
+								setProcessing(true);
 
-            return (
-                <PromptModal
-                    onClose={onClose}
-                    question={<Text
-                        id={`app.special.modals.prompt.${event[0]}`}
-                        fields={{ name }}
-                    />}
-                    actions={[
-                        {
-                            confirmation: true,
-                            contrast: true,
-                            error: true,
-                            text: <Text id={`app.special.modals.actions.${event[1]}`} />,
-                            onClick: async () => {
-                                setProcessing(true);
+								try {
+									await client.channels.deleteMessage(
+										props.target.channel,
+										props.target._id,
+									);
 
-                                try {
-                                    switch (props.type) {
-                                        case 'unfriend_user':
-                                            await client.users.removeFriend(props.target._id); break;
-                                        case 'block_user':
-                                            await client.users.blockUser(props.target._id); break;
-                                        case 'leave_group':
-                                        case 'close_dm':
-                                        case 'delete_channel':
-                                            await client.channels.delete(props.target._id); break;
-                                        case 'leave_server':
-                                        case 'delete_server':
-                                            await client.servers.delete(props.target._id); break;
-                                    }
+									onClose();
+								} catch (err) {
+									setError(takeError(err));
+									setProcessing(false);
+								}
+							},
+						},
+						{
+							text: (
+								<Text id="app.special.modals.actions.cancel" />
+							),
+							onClick: onClose,
+						},
+					]}
+					content={
+						<>
+							<Text
+								id={`app.special.modals.prompt.confirm_delete_message_long`}
+							/>
+							<Message
+								message={mapMessage(props.target)}
+								head={true}
+								contrast
+							/>
+						</>
+					}
+					disabled={processing}
+					error={error}
+				/>
+			);
+		}
+		case "create_invite": {
+			const [code, setCode] = useState("abcdef");
+			const { writeClipboard } = useIntermediate();
 
-                                    onClose();
-                                } catch (err) {
-                                    setError(takeError(err));
-                                    setProcessing(false);
-                                }
-                            }
-                        },
-                        { text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
-                    ]}
-                    content={<TextReact id={`app.special.modals.prompt.${event[0]}_long`} fields={{ name: <b>{ name }</b> }} />}
-                    disabled={processing}
-                    error={error}
-                />
-            )
-        }
-        case 'delete_message': {
-            return (
-                <PromptModal
-                    onClose={onClose}
-                    question={<Text id={'app.context_menu.delete_message'} />}
-                    actions={[
-                        {
-                            confirmation: true,
-                            contrast: true,
-                            error: true,
-                            text: <Text id="app.special.modals.actions.delete" />,
-                            onClick: async () => {
-                                setProcessing(true);
+			useEffect(() => {
+				setProcessing(true);
 
-                                try {
-                                    await client.channels.deleteMessage(props.target.channel, props.target._id);
+				client.channels
+					.createInvite(props.target._id)
+					.then((code) => setCode(code))
+					.catch((err) => setError(takeError(err)))
+					.finally(() => setProcessing(false));
+			}, []);
 
-                                    onClose();
-                                } catch (err) {
-                                    setError(takeError(err));
-                                    setProcessing(false);
-                                }
-                            }
-                        },
-                        { text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
-                    ]}
-                    content={<>
-                        <Text id={`app.special.modals.prompt.confirm_delete_message_long`} />
-                        <Message message={mapMessage(props.target)} head={true} contrast />
-                    </>}
-                    disabled={processing}
-                    error={error}
-                />
-            )
-        }
-        case "create_invite": {
-            const [ code, setCode ] = useState('abcdef');
-            const { writeClipboard } = useIntermediate();
+			return (
+				<PromptModal
+					onClose={onClose}
+					question={<Text id={`app.context_menu.create_invite`} />}
+					actions={[
+						{
+							text: <Text id="app.special.modals.actions.ok" />,
+							confirmation: true,
+							onClick: onClose,
+						},
+						{
+							text: <Text id="app.context_menu.copy_link" />,
+							onClick: () =>
+								writeClipboard(
+									`${window.location.protocol}//${window.location.host}/invite/${code}`,
+								),
+						},
+					]}
+					content={
+						processing ? (
+							<Text id="app.special.modals.prompt.create_invite_generate" />
+						) : (
+							<div className={styles.invite}>
+								<Text id="app.special.modals.prompt.create_invite_created" />
+								<code>{code}</code>
+							</div>
+						)
+					}
+					disabled={processing}
+					error={error}
+				/>
+			);
+		}
+		case "kick_member": {
+			const user = client.users.get(props.user);
 
-            useEffect(() => {
-                setProcessing(true);
+			return (
+				<PromptModal
+					onClose={onClose}
+					question={<Text id={`app.context_menu.kick_member`} />}
+					actions={[
+						{
+							text: <Text id="app.special.modals.actions.kick" />,
+							contrast: true,
+							error: true,
+							confirmation: true,
+							onClick: async () => {
+								setProcessing(true);
 
-                client.channels.createInvite(props.target._id)
-                    .then(code => setCode(code))
-                    .catch(err => setError(takeError(err)))
-                    .finally(() => setProcessing(false));
-            }, []);
+								try {
+									await client.servers.members.kickMember(
+										props.target._id,
+										props.user,
+									);
+									onClose();
+								} catch (err) {
+									setError(takeError(err));
+									setProcessing(false);
+								}
+							},
+						},
+						{
+							text: (
+								<Text id="app.special.modals.actions.cancel" />
+							),
+							onClick: onClose,
+						},
+					]}
+					content={
+						<div className={styles.column}>
+							<UserIcon target={user} size={64} />
+							<Text
+								id="app.special.modals.prompt.confirm_kick"
+								fields={{ name: user?.username }}
+							/>
+						</div>
+					}
+					disabled={processing}
+					error={error}
+				/>
+			);
+		}
+		case "ban_member": {
+			const [reason, setReason] = useState<string | undefined>(undefined);
+			const user = client.users.get(props.user);
 
-            return (
-                <PromptModal
-                    onClose={onClose}
-                    question={<Text id={`app.context_menu.create_invite`} />}
-                    actions={[
-                        {
-                            text: <Text id="app.special.modals.actions.ok" />,
-                            confirmation: true,
-                            onClick: onClose
-                        },
-                        {
-                            text: <Text id="app.context_menu.copy_link" />,
-                            onClick: () => writeClipboard(`${window.location.protocol}//${window.location.host}/invite/${code}`)
-                        }
-                    ]}
-                    content={
-                        processing ?
-                            <Text id="app.special.modals.prompt.create_invite_generate" />
-                          : <div className={styles.invite}>
-                              <Text id="app.special.modals.prompt.create_invite_created" />
-                              <code>{code}</code>
-                            </div>
-                    }
-                    disabled={processing}
-                    error={error}
-                />
-            )
-        }
-        case "kick_member": {
-            const user = client.users.get(props.user);
+			return (
+				<PromptModal
+					onClose={onClose}
+					question={<Text id={`app.context_menu.ban_member`} />}
+					actions={[
+						{
+							text: <Text id="app.special.modals.actions.ban" />,
+							contrast: true,
+							error: true,
+							confirmation: true,
+							onClick: async () => {
+								setProcessing(true);
 
-            return (
-                <PromptModal
-                    onClose={onClose}
-                    question={<Text id={`app.context_menu.kick_member`} />}
-                    actions={[
-                        {
-                            text: <Text id="app.special.modals.actions.kick" />,
-                            contrast: true,
-                            error: true,
-                            confirmation: true,
-                            onClick: async () => {
-                                setProcessing(true);
-                                
-                                try {
-                                    await client.servers.members.kickMember(props.target._id, props.user);
-                                    onClose();
-                                } catch (err) {
-                                    setError(takeError(err));
-                                    setProcessing(false);
-                                }
-                            }
-                        },
-                        { text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
-                    ]}
-                    content={<div className={styles.column}>
-                        <UserIcon target={user} size={64} />
-                        <Text
-                            id="app.special.modals.prompt.confirm_kick"
-                            fields={{ name: user?.username }} />
-                    </div>}
-                    disabled={processing}
-                    error={error}
-                />
-            )
-        }
-        case "ban_member": {
-            const [ reason, setReason ] = useState<string | undefined>(undefined);
-            const user = client.users.get(props.user);
+								try {
+									await client.servers.banUser(
+										props.target._id,
+										props.user,
+										{ reason },
+									);
+									onClose();
+								} catch (err) {
+									setError(takeError(err));
+									setProcessing(false);
+								}
+							},
+						},
+						{
+							text: (
+								<Text id="app.special.modals.actions.cancel" />
+							),
+							onClick: onClose,
+						},
+					]}
+					content={
+						<div className={styles.column}>
+							<UserIcon target={user} size={64} />
+							<Text
+								id="app.special.modals.prompt.confirm_ban"
+								fields={{ name: user?.username }}
+							/>
+							<Overline>
+								<Text id="app.special.modals.prompt.confirm_ban_reason" />
+							</Overline>
+							<InputBox
+								value={reason ?? ""}
+								onChange={(e) =>
+									setReason(e.currentTarget.value)
+								}
+							/>
+						</div>
+					}
+					disabled={processing}
+					error={error}
+				/>
+			);
+		}
+		case "create_channel": {
+			const [name, setName] = useState("");
+			const [type, setType] = useState<"Text" | "Voice">("Text");
+			const history = useHistory();
 
-            return (
-                <PromptModal
-                    onClose={onClose}
-                    question={<Text id={`app.context_menu.ban_member`} />}
-                    actions={[
-                        {
-                            text: <Text id="app.special.modals.actions.ban" />,
-                            contrast: true,
-                            error: true,
-                            confirmation: true,
-                            onClick: async () => {
-                                setProcessing(true);
-                                
-                                try {
-                                    await client.servers.banUser(props.target._id, props.user, { reason });
-                                    onClose();
-                                } catch (err) {
-                                    setError(takeError(err));
-                                    setProcessing(false);
-                                }
-                            }
-                        },
-                        { text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
-                    ]}
-                    content={<div className={styles.column}>
-                        <UserIcon target={user} size={64} />
-                        <Text
-                            id="app.special.modals.prompt.confirm_ban"
-                            fields={{ name: user?.username }} />
-                        <Overline><Text id="app.special.modals.prompt.confirm_ban_reason" /></Overline>
-                        <InputBox value={reason ?? ''} onChange={e => setReason(e.currentTarget.value)} />
-                    </div>}
-                    disabled={processing}
-                    error={error}
-                />
-            )
-        }
-        case 'create_channel': {
-            const [ name, setName ] = useState('');
-            const [ type, setType ] = useState<'Text' | 'Voice'>('Text');
-            const history = useHistory();
+			return (
+				<PromptModal
+					onClose={onClose}
+					question={<Text id="app.context_menu.create_channel" />}
+					actions={[
+						{
+							confirmation: true,
+							contrast: true,
+							text: (
+								<Text id="app.special.modals.actions.create" />
+							),
+							onClick: async () => {
+								setProcessing(true);
 
-            return (
-                <PromptModal
-                    onClose={onClose}
-                    question={<Text id="app.context_menu.create_channel" />}
-                    actions={[
-                        {
-                            confirmation: true,
-                            contrast: true,
-                            text: <Text id="app.special.modals.actions.create" />,
-                            onClick: async () => {
-                                setProcessing(true);
+								try {
+									const channel =
+										await client.servers.createChannel(
+											props.target._id,
+											{
+												type,
+												name,
+												nonce: ulid(),
+											},
+										);
 
-                                try {
-                                    const channel = await client.servers.createChannel(
-                                        props.target._id,
-                                        {
-                                            type,
-                                            name,
-                                            nonce: ulid()
-                                        }
-                                    );
-                
-                                    history.push(`/server/${props.target._id}/channel/${channel._id}`);
-                                    onClose();
-                                } catch (err) {
-                                    setError(takeError(err));
-                                    setProcessing(false);
-                                }
-                            }
-                        },
-                        { text: <Text id="app.special.modals.actions.cancel" />, onClick: onClose }
-                    ]}
-                    content={<>
-                        <Overline block type="subtle"><Text id="app.main.servers.channel_type" /></Overline>
-                        <Radio checked={type === 'Text'} onSelect={() => setType('Text')}>
-                            <Text id="app.main.servers.text_channel" /></Radio>
-                        <Radio checked={type === 'Voice'} onSelect={() => setType('Voice')}>
-                            <Text id="app.main.servers.voice_channel" /></Radio>
-                        <Overline block type="subtle"><Text id="app.main.servers.channel_name" /></Overline>
-                        <InputBox
-                            value={name}
-                            onChange={e => setName(e.currentTarget.value)} />
-                    </>}
-                    disabled={processing}
-                    error={error}
-                />
-            )
-        }
-        default: return null;
-    }
+									history.push(
+										`/server/${props.target._id}/channel/${channel._id}`,
+									);
+									onClose();
+								} catch (err) {
+									setError(takeError(err));
+									setProcessing(false);
+								}
+							},
+						},
+						{
+							text: (
+								<Text id="app.special.modals.actions.cancel" />
+							),
+							onClick: onClose,
+						},
+					]}
+					content={
+						<>
+							<Overline block type="subtle">
+								<Text id="app.main.servers.channel_type" />
+							</Overline>
+							<Radio
+								checked={type === "Text"}
+								onSelect={() => setType("Text")}>
+								<Text id="app.main.servers.text_channel" />
+							</Radio>
+							<Radio
+								checked={type === "Voice"}
+								onSelect={() => setType("Voice")}>
+								<Text id="app.main.servers.voice_channel" />
+							</Radio>
+							<Overline block type="subtle">
+								<Text id="app.main.servers.channel_name" />
+							</Overline>
+							<InputBox
+								value={name}
+								onChange={(e) => setName(e.currentTarget.value)}
+							/>
+						</>
+					}
+					disabled={processing}
+					error={error}
+				/>
+			);
+		}
+		default:
+			return null;
+	}
 }
diff --git a/src/context/intermediate/modals/SignedOut.tsx b/src/context/intermediate/modals/SignedOut.tsx
index b56f63856a15fe32327fd7ffd6161980b3fd5cfa..e04ba1038c4e1d6f7c2650d3bd612e723fc2e202 100644
--- a/src/context/intermediate/modals/SignedOut.tsx
+++ b/src/context/intermediate/modals/SignedOut.tsx
@@ -1,23 +1,24 @@
 import { Text } from "preact-i18n";
+
 import Modal from "../../../components/ui/Modal";
 
 interface Props {
-    onClose: () => void;
+	onClose: () => void;
 }
 
 export function SignedOutModal({ onClose }: Props) {
-    return (
-        <Modal
-            visible={true}
-            onClose={onClose}
-            title={<Text id="app.special.modals.signed_out" />}
-            actions={[
-                {
-                    onClick: onClose,
-                    confirmation: true,
-                    text: <Text id="app.special.modals.actions.ok" />
-                }
-            ]}
-        />
-    );
+	return (
+		<Modal
+			visible={true}
+			onClose={onClose}
+			title={<Text id="app.special.modals.signed_out" />}
+			actions={[
+				{
+					onClick: onClose,
+					confirmation: true,
+					text: <Text id="app.special.modals.actions.ok" />,
+				},
+			]}
+		/>
+	);
 }
diff --git a/src/context/intermediate/popovers/ChannelInfo.tsx b/src/context/intermediate/popovers/ChannelInfo.tsx
index 8b0b7ddb49c0a8bd2932a2d04bf140b0a802041c..f10c14537589707627d3ae82eb2c94341b30aa39 100644
--- a/src/context/intermediate/popovers/ChannelInfo.tsx
+++ b/src/context/intermediate/popovers/ChannelInfo.tsx
@@ -1,38 +1,44 @@
 import { X } from "@styled-icons/boxicons-regular";
+
 import styles from "./ChannelInfo.module.scss";
+
 import Modal from "../../../components/ui/Modal";
-import { getChannelName } from "../../revoltjs/util";
+
 import Markdown from "../../../components/markdown/Markdown";
 import { useChannel, useForceUpdate } from "../../revoltjs/hooks";
+import { getChannelName } from "../../revoltjs/util";
 
 interface Props {
-    channel_id: string;
-    onClose: () => void;
+	channel_id: string;
+	onClose: () => void;
 }
 
 export function ChannelInfo({ channel_id, onClose }: Props) {
-    const ctx = useForceUpdate();
-    const channel = useChannel(channel_id, ctx);
-    if (!channel) return null;
+	const ctx = useForceUpdate();
+	const channel = useChannel(channel_id, ctx);
+	if (!channel) return null;
 
-    if (channel.channel_type === "DirectMessage" || channel.channel_type === 'SavedMessages') {
-        onClose();
-        return null;
-    }
+	if (
+		channel.channel_type === "DirectMessage" ||
+		channel.channel_type === "SavedMessages"
+	) {
+		onClose();
+		return null;
+	}
 
-    return (
-        <Modal visible={true} onClose={onClose}>
-            <div className={styles.info}>
-                <div className={styles.header}>
-                    <h1>{ getChannelName(ctx.client, channel, true) }</h1>
-                    <div onClick={onClose}>
-                        <X size={36} />
-                    </div>
-                </div>
-                <p>
-                    <Markdown content={channel.description} />
-                </p>
-            </div>
-        </Modal>
-    );
+	return (
+		<Modal visible={true} onClose={onClose}>
+			<div className={styles.info}>
+				<div className={styles.header}>
+					<h1>{getChannelName(ctx.client, channel, true)}</h1>
+					<div onClick={onClose}>
+						<X size={36} />
+					</div>
+				</div>
+				<p>
+					<Markdown content={channel.description} />
+				</p>
+			</div>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/popovers/ImageViewer.tsx b/src/context/intermediate/popovers/ImageViewer.tsx
index d0cf0cfc1dedbb6f4b987f01ceea74536bed29b6..a199ba70e11b6ceb1344568e64b821e7dac642a0 100644
--- a/src/context/intermediate/popovers/ImageViewer.tsx
+++ b/src/context/intermediate/popovers/ImageViewer.tsx
@@ -1,43 +1,46 @@
+import { Attachment, EmbedImage } from "revolt.js/dist/api/objects";
+
 import styles from "./ImageViewer.module.scss";
-import Modal from "../../../components/ui/Modal";
 import { useContext, useEffect } from "preact/hooks";
-import { AppContext } from "../../revoltjs/RevoltClient";
-import { Attachment, EmbedImage } from "revolt.js/dist/api/objects";
-import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
+
 import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
+import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
+import Modal from "../../../components/ui/Modal";
+
+import { AppContext } from "../../revoltjs/RevoltClient";
 
 interface Props {
-    onClose: () => void;
-    embed?: EmbedImage;
-    attachment?: Attachment;
+	onClose: () => void;
+	embed?: EmbedImage;
+	attachment?: Attachment;
 }
 
 export function ImageViewer({ attachment, embed, onClose }: Props) {
-    // ! FIXME: temp code
-    // ! add proxy function to client
-    function proxyImage(url: string) {
-        return 'https://jan.revolt.chat/proxy?url=' + encodeURIComponent(url);
-    }
-    
-    if (attachment && attachment.metadata.type !== "Image") return null;
-    const client = useContext(AppContext);
+	// ! FIXME: temp code
+	// ! add proxy function to client
+	function proxyImage(url: string) {
+		return "https://jan.revolt.chat/proxy?url=" + encodeURIComponent(url);
+	}
+
+	if (attachment && attachment.metadata.type !== "Image") return null;
+	const client = useContext(AppContext);
 
-    return (
-        <Modal visible={true} onClose={onClose} noBackground>
-            <div className={styles.viewer}>
-                { attachment &&
-                    <>
-                        <img src={client.generateFileURL(attachment)} />
-                        <AttachmentActions attachment={attachment} />
-                    </>
-                }
-                { embed &&
-                    <>
-                        <img src={proxyImage(embed.url)} />
-                        <EmbedMediaActions embed={embed} />
-                    </>
-                }
-            </div>
-        </Modal>
-    );
+	return (
+		<Modal visible={true} onClose={onClose} noBackground>
+			<div className={styles.viewer}>
+				{attachment && (
+					<>
+						<img src={client.generateFileURL(attachment)} />
+						<AttachmentActions attachment={attachment} />
+					</>
+				)}
+				{embed && (
+					<>
+						<img src={proxyImage(embed.url)} />
+						<EmbedMediaActions embed={embed} />
+					</>
+				)}
+			</div>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/popovers/ModifyAccount.tsx b/src/context/intermediate/popovers/ModifyAccount.tsx
index 25477c0768b99a61149b2c12d8103afca363fb9f..b719554a39cc4d2f5d2835645edc99db4c129582 100644
--- a/src/context/intermediate/popovers/ModifyAccount.tsx
+++ b/src/context/intermediate/popovers/ModifyAccount.tsx
@@ -1,127 +1,134 @@
-import { Text } from "preact-i18n";
 import { SubmitHandler, useForm } from "react-hook-form";
-import Modal from "../../../components/ui/Modal";
-import { takeError } from "../../revoltjs/util";
+
+import { Text } from "preact-i18n";
 import { useContext, useState } from "preact/hooks";
-import FormField from '../../../pages/login/FormField';
+
+import Modal from "../../../components/ui/Modal";
 import Overline from "../../../components/ui/Overline";
+
+import FormField from "../../../pages/login/FormField";
 import { AppContext } from "../../revoltjs/RevoltClient";
+import { takeError } from "../../revoltjs/util";
 
 interface Props {
-    onClose: () => void;
-    field: "username" | "email" | "password";
+	onClose: () => void;
+	field: "username" | "email" | "password";
 }
 
 interface FormInputs {
-    password: string,
-    new_email: string,
-    new_username: string,
-    new_password: string,
+	password: string;
+	new_email: string;
+	new_username: string;
+	new_password: string;
 
-    // TODO: figure out if this is correct or not
-    // it wasn't in the types before this was typed but the element itself was there
-    current_password?: string
+	// TODO: figure out if this is correct or not
+	// it wasn't in the types before this was typed but the element itself was there
+	current_password?: string;
 }
 
 export function ModifyAccountModal({ onClose, field }: Props) {
-    const client = useContext(AppContext);
-    const { handleSubmit, register, errors } = useForm<FormInputs>();
-    const [error, setError] = useState<string | undefined>(undefined);
+	const client = useContext(AppContext);
+	const { handleSubmit, register, errors } = useForm<FormInputs>();
+	const [error, setError] = useState<string | undefined>(undefined);
 
-    const onSubmit: SubmitHandler<FormInputs> = async ({
-        password,
-        new_username,
-        new_email,
-        new_password
-    }) => {
-        try {
-            if (field === "email") {
-                await client.req("POST", "/auth/change/email", {
-                    password,
-                    new_email
-                });
-                onClose();
-            } else if (field === "password") {
-                await client.req("POST", "/auth/change/password", {
-                    password,
-                    new_password
-                });
-                onClose();
-            } else if (field === "username") {
-                await client.req("PATCH", "/users/id/username", {
-                    username: new_username,
-                    password
-                });
-                onClose();
-            }
-        } catch (err) {
-            setError(takeError(err));
-        }
-    }
+	const onSubmit: SubmitHandler<FormInputs> = async ({
+		password,
+		new_username,
+		new_email,
+		new_password,
+	}) => {
+		try {
+			if (field === "email") {
+				await client.req("POST", "/auth/change/email", {
+					password,
+					new_email,
+				});
+				onClose();
+			} else if (field === "password") {
+				await client.req("POST", "/auth/change/password", {
+					password,
+					new_password,
+				});
+				onClose();
+			} else if (field === "username") {
+				await client.req("PATCH", "/users/id/username", {
+					username: new_username,
+					password,
+				});
+				onClose();
+			}
+		} catch (err) {
+			setError(takeError(err));
+		}
+	};
 
-    return (
-        <Modal
-            visible={true}
-            onClose={onClose}
-            title={<Text id={`app.special.modals.account.change.${field}`} />}
-            actions={[
-                {
-                    confirmation: true,
-                    onClick: handleSubmit(onSubmit),
-                    text:
-                        field === "email" ? (
-                            <Text id="app.special.modals.actions.send_email" />
-                        ) : (
-                            <Text id="app.special.modals.actions.update" />
-                        )
-                },
-                {
-                    onClick: onClose,
-                    text: <Text id="app.special.modals.actions.close" />
-                }
-            ]}
-        >
-            {/* Preact / React typing incompatabilities */}
-            <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
-                {field === "email" && (
-                    <FormField
-                        type="email"
-                        name="new_email"
-                        register={register}
-                        showOverline
-                        error={errors.new_email?.message}
-                    />
-                )}
-                {field === "password" && (
-                    <FormField
-                        type="password"
-                        name="new_password"
-                        register={register}
-                        showOverline
-                        error={errors.new_password?.message}
-                    />
-                )}
-                {field === "username" && (
-                    <FormField
-                        type="username"
-                        name="new_username"
-                        register={register}
-                        showOverline
-                        error={errors.new_username?.message}
-                    />
-                )}
-                <FormField
-                    type="current_password"
-                    register={register}
-                    showOverline
-                    error={errors.current_password?.message}
-                />
-                {error && (
-                    <Overline type="error" error={error}>
-                        <Text id="app.special.modals.account.failed" />
-                    </Overline>
-                )}
-            </form>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible={true}
+			onClose={onClose}
+			title={<Text id={`app.special.modals.account.change.${field}`} />}
+			actions={[
+				{
+					confirmation: true,
+					onClick: handleSubmit(onSubmit),
+					text:
+						field === "email" ? (
+							<Text id="app.special.modals.actions.send_email" />
+						) : (
+							<Text id="app.special.modals.actions.update" />
+						),
+				},
+				{
+					onClick: onClose,
+					text: <Text id="app.special.modals.actions.close" />,
+				},
+			]}>
+			{/* Preact / React typing incompatabilities */}
+			<form
+				onSubmit={
+					handleSubmit(
+						onSubmit,
+					) as JSX.GenericEventHandler<HTMLFormElement>
+				}>
+				{field === "email" && (
+					<FormField
+						type="email"
+						name="new_email"
+						register={register}
+						showOverline
+						error={errors.new_email?.message}
+					/>
+				)}
+				{field === "password" && (
+					<FormField
+						type="password"
+						name="new_password"
+						register={register}
+						showOverline
+						error={errors.new_password?.message}
+					/>
+				)}
+				{field === "username" && (
+					<FormField
+						type="username"
+						name="new_username"
+						register={register}
+						showOverline
+						error={errors.new_username?.message}
+					/>
+				)}
+				<FormField
+					type="current_password"
+					register={register}
+					showOverline
+					error={errors.current_password?.message}
+				/>
+				{error && (
+					<Overline type="error" error={error}>
+						<Text id="app.special.modals.account.failed" />
+					</Overline>
+				)}
+			</form>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/popovers/PendingRequests.tsx b/src/context/intermediate/popovers/PendingRequests.tsx
index 5b6a68b843d45a4fd03feb65e552e219808301b8..424e32683dfa86b4f52e2be9d787099e48cc007f 100644
--- a/src/context/intermediate/popovers/PendingRequests.tsx
+++ b/src/context/intermediate/popovers/PendingRequests.tsx
@@ -1,27 +1,31 @@
-import { Text } from "preact-i18n";
 import styles from "./UserPicker.module.scss";
-import { useUsers } from "../../revoltjs/hooks";
+import { Text } from "preact-i18n";
+
 import Modal from "../../../components/ui/Modal";
+
 import { Friend } from "../../../pages/friends/Friend";
+import { useUsers } from "../../revoltjs/hooks";
 
 interface Props {
-    users: string[];
-    onClose: () => void;
+	users: string[];
+	onClose: () => void;
 }
 
 export function PendingRequests({ users: ids, onClose }: Props) {
-    const users = useUsers(ids);
+	const users = useUsers(ids);
 
-    return (
-        <Modal
-            visible={true}
-            title={<Text id="app.special.friends.pending" />}
-            onClose={onClose}>
-            <div className={styles.list}>
-                { users
-                    .filter(x => typeof x !== 'undefined')
-                    .map(x => <Friend user={x!} key={x!._id} />) }
-            </div>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible={true}
+			title={<Text id="app.special.friends.pending" />}
+			onClose={onClose}>
+			<div className={styles.list}>
+				{users
+					.filter((x) => typeof x !== "undefined")
+					.map((x) => (
+						<Friend user={x!} key={x!._id} />
+					))}
+			</div>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/popovers/UserPicker.tsx b/src/context/intermediate/popovers/UserPicker.tsx
index b4ab76ca90139f9cdf3eb4ac4a0e42305cd86075..09f8e403e894db108921a9b2c11fc24bbd70cc75 100644
--- a/src/context/intermediate/popovers/UserPicker.tsx
+++ b/src/context/intermediate/popovers/UserPicker.tsx
@@ -1,64 +1,68 @@
+import { User, Users } from "revolt.js/dist/api/objects";
+
+import styles from "./UserPicker.module.scss";
 import { Text } from "preact-i18n";
 import { useState } from "preact/hooks";
-import styles from "./UserPicker.module.scss";
-import { useUsers } from "../../revoltjs/hooks";
-import Modal from "../../../components/ui/Modal";
-import { User, Users } from "revolt.js/dist/api/objects";
+
 import UserCheckbox from "../../../components/common/user/UserCheckbox";
+import Modal from "../../../components/ui/Modal";
+
+import { useUsers } from "../../revoltjs/hooks";
 
 interface Props {
-    omit?: string[];
-    onClose: () => void;
-    callback: (users: string[]) => Promise<void>;
+	omit?: string[];
+	onClose: () => void;
+	callback: (users: string[]) => Promise<void>;
 }
 
 export function UserPicker(props: Props) {
-    const [selected, setSelected] = useState<string[]>([]);
-    const omit = [...(props.omit || []), "00000000000000000000000000"];
+	const [selected, setSelected] = useState<string[]>([]);
+	const omit = [...(props.omit || []), "00000000000000000000000000"];
 
-    const users = useUsers();
+	const users = useUsers();
 
-    return (
-        <Modal
-            visible={true}
-            title={<Text id="app.special.popovers.user_picker.select" />}
-            onClose={props.onClose}
-            actions={[
-                {
-                    text: <Text id="app.special.modals.actions.ok" />,
-                    onClick: () => props.callback(selected).then(props.onClose)
-                }
-            ]}
-        >
-            <div className={styles.list}>
-                {(users.filter(
-                    x =>
-                        x &&
-                        x.relationship === Users.Relationship.Friend &&
-                        !omit.includes(x._id)
-                ) as User[])
-                    .map(x => {
-                        return {
-                            ...x,
-                            selected: selected.includes(x._id)
-                        };
-                    })
-                    .map(x => (
-                        <UserCheckbox
-                            user={x}
-                            checked={x.selected}
-                            onChange={v => {
-                                if (v) {
-                                    setSelected([...selected, x._id]);
-                                } else {
-                                    setSelected(
-                                        selected.filter(y => y !== x._id)
-                                    );
-                                }
-                            }}
-                        />
-                    ))}
-            </div>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible={true}
+			title={<Text id="app.special.popovers.user_picker.select" />}
+			onClose={props.onClose}
+			actions={[
+				{
+					text: <Text id="app.special.modals.actions.ok" />,
+					onClick: () => props.callback(selected).then(props.onClose),
+				},
+			]}>
+			<div className={styles.list}>
+				{(
+					users.filter(
+						(x) =>
+							x &&
+							x.relationship === Users.Relationship.Friend &&
+							!omit.includes(x._id),
+					) as User[]
+				)
+					.map((x) => {
+						return {
+							...x,
+							selected: selected.includes(x._id),
+						};
+					})
+					.map((x) => (
+						<UserCheckbox
+							user={x}
+							checked={x.selected}
+							onChange={(v) => {
+								if (v) {
+									setSelected([...selected, x._id]);
+								} else {
+									setSelected(
+										selected.filter((y) => y !== x._id),
+									);
+								}
+							}}
+						/>
+					))}
+			</div>
+		</Modal>
+	);
 }
diff --git a/src/context/intermediate/popovers/UserProfile.tsx b/src/context/intermediate/popovers/UserProfile.tsx
index e8a22e5b1ee37f4c6960f4bb0f8e7be2e0ae366d..beb2833359be5592b664a7888c64f8ffb158e9a4 100644
--- a/src/context/intermediate/popovers/UserProfile.tsx
+++ b/src/context/intermediate/popovers/UserProfile.tsx
@@ -1,331 +1,362 @@
-import { decodeTime } from "ulid";
+import {
+	Envelope,
+	Edit,
+	UserPlus,
+	Shield,
+	Money,
+} from "@styled-icons/boxicons-regular";
 import { Link, useHistory } from "react-router-dom";
-import { Localizer, Text } from "preact-i18n";
-import styles from "./UserProfile.module.scss";
-import Modal from "../../../components/ui/Modal";
-import { Route } from "revolt.js/dist/api/routes";
 import { Users } from "revolt.js/dist/api/objects";
-import { useIntermediate } from "../Intermediate";
-import Preloader from "../../../components/ui/Preloader";
-import Tooltip from '../../../components/common/Tooltip';
-import IconButton from "../../../components/ui/IconButton";
-import Markdown from '../../../components/markdown/Markdown';
 import { UserPermission } from "revolt.js/dist/api/permissions";
-import UserIcon from '../../../components/common/user/UserIcon';
-import ChannelIcon from '../../../components/common/ChannelIcon';
-import UserStatus from '../../../components/common/user/UserStatus';
-import { Envelope, Edit, UserPlus, Shield, Money } from "@styled-icons/boxicons-regular";
+import { Route } from "revolt.js/dist/api/routes";
+import { decodeTime } from "ulid";
+
+import styles from "./UserProfile.module.scss";
+import { Localizer, Text } from "preact-i18n";
 import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
-import { AppContext, ClientStatus, StatusContext } from "../../revoltjs/RevoltClient";
-import { useChannels, useForceUpdate, useUserPermission, useUsers } from "../../revoltjs/hooks";
+
+import ChannelIcon from "../../../components/common/ChannelIcon";
+import Tooltip from "../../../components/common/Tooltip";
+import UserIcon from "../../../components/common/user/UserIcon";
+import UserStatus from "../../../components/common/user/UserStatus";
+import IconButton from "../../../components/ui/IconButton";
+import Modal from "../../../components/ui/Modal";
+import Preloader from "../../../components/ui/Preloader";
+
+import Markdown from "../../../components/markdown/Markdown";
+import {
+	AppContext,
+	ClientStatus,
+	StatusContext,
+} from "../../revoltjs/RevoltClient";
+import {
+	useChannels,
+	useForceUpdate,
+	useUserPermission,
+	useUsers,
+} from "../../revoltjs/hooks";
+import { useIntermediate } from "../Intermediate";
 
 interface Props {
-    user_id: string;
-    dummy?: boolean;
-    onClose: () => void;
-    dummyProfile?: Users.Profile;
+	user_id: string;
+	dummy?: boolean;
+	onClose: () => void;
+	dummyProfile?: Users.Profile;
 }
 
 enum Badges {
-    Developer = 1,
-    Translator = 2,
-    Supporter = 4,
-    ResponsibleDisclosure = 8,
-    EarlyAdopter = 256
+	Developer = 1,
+	Translator = 2,
+	Supporter = 4,
+	ResponsibleDisclosure = 8,
+	EarlyAdopter = 256,
 }
 
 export function UserProfile({ user_id, onClose, dummy, dummyProfile }: Props) {
-    const { openScreen, writeClipboard } = useIntermediate();
+	const { openScreen, writeClipboard } = useIntermediate();
+
+	const [profile, setProfile] = useState<undefined | null | Users.Profile>(
+		undefined,
+	);
+	const [mutual, setMutual] = useState<
+		undefined | null | Route<"GET", "/users/id/mutual">["response"]
+	>(undefined);
+
+	const history = useHistory();
+	const client = useContext(AppContext);
+	const status = useContext(StatusContext);
+	const [tab, setTab] = useState("profile");
 
-    const [profile, setProfile] = useState<undefined | null | Users.Profile>(
-        undefined
-    );
-    const [mutual, setMutual] = useState<
-        undefined | null | Route<"GET", "/users/id/mutual">["response"]
-    >(undefined);
+	const ctx = useForceUpdate();
+	const all_users = useUsers(undefined, ctx);
+	const channels = useChannels(undefined, ctx);
 
-    const history = useHistory();
-    const client = useContext(AppContext);
-    const status = useContext(StatusContext);
-    const [tab, setTab] = useState("profile");
+	const user = all_users.find((x) => x!._id === user_id);
+	const users = mutual?.users
+		? all_users.filter((x) => mutual.users.includes(x!._id))
+		: undefined;
 
-    const ctx = useForceUpdate();
-    const all_users = useUsers(undefined, ctx);
-    const channels = useChannels(undefined, ctx);
-    
-    const user = all_users.find(x => x!._id === user_id);
-    const users = mutual?.users ? all_users.filter(x => mutual.users.includes(x!._id)) : undefined;
-    
-    if (!user) {
-        useEffect(onClose, []);
-        return null;
-    }
+	if (!user) {
+		useEffect(onClose, []);
+		return null;
+	}
 
-    const permissions = useUserPermission(user!._id, ctx);
+	const permissions = useUserPermission(user!._id, ctx);
 
-    useLayoutEffect(() => {
-        if (!user_id) return;
-        if (typeof profile !== 'undefined') setProfile(undefined);
-        if (typeof mutual  !== 'undefined') setMutual(undefined);
-    }, [user_id]);
+	useLayoutEffect(() => {
+		if (!user_id) return;
+		if (typeof profile !== "undefined") setProfile(undefined);
+		if (typeof mutual !== "undefined") setMutual(undefined);
+	}, [user_id]);
 
-    if (dummy) {
-        useLayoutEffect(() => {
-            setProfile(dummyProfile);
-        }, [dummyProfile]);
-    }
+	if (dummy) {
+		useLayoutEffect(() => {
+			setProfile(dummyProfile);
+		}, [dummyProfile]);
+	}
 
-    useEffect(() => {
-        if (dummy) return;
-        if (
-            status === ClientStatus.ONLINE &&
-            typeof mutual === "undefined"
-        ) {
-            setMutual(null);
-            client.users
-                .fetchMutual(user_id)
-                .then(data => setMutual(data));
-        }
-    }, [mutual, status]);
+	useEffect(() => {
+		if (dummy) return;
+		if (status === ClientStatus.ONLINE && typeof mutual === "undefined") {
+			setMutual(null);
+			client.users.fetchMutual(user_id).then((data) => setMutual(data));
+		}
+	}, [mutual, status]);
 
-    useEffect(() => {
-        if (dummy) return;
-        if (
-            status === ClientStatus.ONLINE &&
-            typeof profile === "undefined"
-        ) {
-            setProfile(null);
+	useEffect(() => {
+		if (dummy) return;
+		if (status === ClientStatus.ONLINE && typeof profile === "undefined") {
+			setProfile(null);
 
-            if (permissions & UserPermission.ViewProfile) {
-                client.users
-                    .fetchProfile(user_id)
-                    .then(data => setProfile(data))
-                    .catch(() => {});
-            }
-        }
-    }, [profile, status]);
+			if (permissions & UserPermission.ViewProfile) {
+				client.users
+					.fetchProfile(user_id)
+					.then((data) => setProfile(data))
+					.catch(() => {});
+			}
+		}
+	}, [profile, status]);
 
-    const mutualGroups = channels.filter(
-        channel =>
-            channel?.channel_type === "Group" &&
-            channel.recipients.includes(user_id)
-    );
+	const mutualGroups = channels.filter(
+		(channel) =>
+			channel?.channel_type === "Group" &&
+			channel.recipients.includes(user_id),
+	);
 
-    const backgroundURL = profile && client.users.getBackgroundURL(profile, { width: 1000 }, true);
-    const badges = (user.badges ?? 0) | (decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
+	const backgroundURL =
+		profile &&
+		client.users.getBackgroundURL(profile, { width: 1000 }, true);
+	const badges =
+		(user.badges ?? 0) |
+		(decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
 
-    return (
-        <Modal visible
-            border={dummy}
-            padding={false}
-            onClose={onClose}
-            dontModal={dummy}>
-            <div
-                className={styles.header}
-                data-force={
-                    profile?.background
-                        ? "light"
-                        : undefined
-                }
-                style={{
-                    backgroundImage: backgroundURL && `linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`
-                }}
-            >
-                <div className={styles.profile}>
-                    <UserIcon size={80} target={user} status />
-                    <div className={styles.details}>
-                        <Localizer>
-                            <span
-                                className={styles.username}
-                                onClick={() => writeClipboard(user.username)}>
-                                @{user.username}
-                            </span>
-                        </Localizer>
-                        {user.status?.text && (
-                            <span className={styles.status}>
-                                <UserStatus user={user} />
-                            </span>
-                        )}
-                    </div>
-                    {user.relationship === Users.Relationship.Friend && (
-                        <Localizer>
-                            <Tooltip
-                                content={
-                                    <Text id="app.context_menu.message_user" />
-                                }
-                            >
-                                <IconButton
-                                    onClick={() => {
-                                        onClose();
-                                        history.push(`/open/${user_id}`);
-                                    }}>
-                                    <Envelope size={30} />
-                                </IconButton>
-                            </Tooltip>
-                        </Localizer>
-                    )}
-                    {user.relationship === Users.Relationship.User && (
-                        <IconButton
-                            onClick={() => {
-                                onClose();
-                                if (dummy) return;
-                                history.push(`/settings/profile`);
-                            }}>
-                            <Edit size={28} />
-                        </IconButton>
-                    )}
-                    {(user.relationship === Users.Relationship.Incoming ||
-                        user.relationship === Users.Relationship.None) && (
-                        <IconButton onClick={() => client.users.addFriend(user.username)}>
-                            <UserPlus size={28} />
-                        </IconButton>
-                    )}
-                </div>
-                <div className={styles.tabs}>
-                    <div
-                        data-active={tab === "profile"}
-                        onClick={() => setTab("profile")}
-                    >
-                        <Text id="app.special.popovers.user_profile.profile" />
-                    </div>
-                    { user.relationship !== Users.Relationship.User &&
-                        <>
-                            <div
-                                data-active={tab === "friends"}
-                                onClick={() => setTab("friends")}
-                            >
-                                <Text id="app.special.popovers.user_profile.mutual_friends" />
-                            </div>
-                            <div
-                                data-active={tab === "groups"}
-                                onClick={() => setTab("groups")}
-                            >
-                                <Text id="app.special.popovers.user_profile.mutual_groups" />
-                            </div>
-                        </>
-                    }
-                </div>
-            </div>
-            <div className={styles.content}>
-                {tab === "profile" &&
-                <div>
-                    { !(profile?.content || (badges > 0)) &&
-                        <div className={styles.empty}><Text id="app.special.popovers.user_profile.empty" /></div> }
-                    { (badges > 0) && <div className={styles.category}><Text id="app.special.popovers.user_profile.sub.badges" /></div> }
-                    { (badges > 0) && (
-                        <div className={styles.badges}>
-                            <Localizer>
-                                {badges & Badges.Developer ? (
-                                    <Tooltip
-                                        content={
-                                            <Text id="app.navigation.tabs.dev" />
-                                        }
-                                    >
-                                        <img src="/assets/badges/developer.svg" />
-                                    </Tooltip>
-                                ) : (
-                                    <></>
-                                )}
-                                {badges & Badges.Translator ? (
-                                    <Tooltip
-                                        content={
-                                            <Text id="app.special.popovers.user_profile.badges.translator" />
-                                        }
-                                    >
-                                        <img src="/assets/badges/translator.svg" />
-                                    </Tooltip>
-                                ) : (
-                                    <></>
-                                )}
-                                {badges & Badges.EarlyAdopter ? (
-                                    <Tooltip
-                                        content={
-                                            <Text id="app.special.popovers.user_profile.badges.early_adopter" />
-                                        }
-                                    >
-                                        <img src="/assets/badges/early_adopter.svg" />
-                                    </Tooltip>
-                                ) : (
-                                    <></>
-                                )}
-                                {badges & Badges.Supporter ? (
-                                    <Tooltip
-                                        content={
-                                            <Text id="app.special.popovers.user_profile.badges.supporter" />
-                                        }
-                                    >
-                                        <Money size={32} color="#efab44" />
-                                    </Tooltip>
-                                ) : (
-                                    <></>
-                                )}
-                                {badges & Badges.ResponsibleDisclosure ? (
-                                    <Tooltip
-                                        content={
-                                            <Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
-                                        }
-                                    >
-                                        <Shield size={32} color="gray" />
-                                    </Tooltip>
-                                ) : (
-                                    <></>
-                                )}
-                            </Localizer>
-                        </div>
-                    )}
-                    { profile?.content && <div className={styles.category}><Text id="app.special.popovers.user_profile.sub.information" /></div> }
-                    <Markdown content={profile?.content} />
-                    {/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
-                </div>}
-                {tab === "friends" &&
-                    (users ? (
-                        <div className={styles.entries}>
-                            {users.length === 0 ? (
-                                <div className={styles.empty}>
-                                    <Text id="app.special.popovers.user_profile.no_users" />
-                                </div>
-                            ) : (
-                                users.map(
-                                    x =>
-                                        x && (
-                                            <div onClick={() => openScreen({ id: 'profile', user_id: x._id })}
-                                                className={styles.entry}
-                                                key={x._id}>
-                                                <UserIcon size={32} target={x} />
-                                                <span>{x.username}</span>
-                                            </div>
-                                        )
-                                )
-                            )}
-                        </div>
-                    ) : (
-                        <Preloader type="ring" />
-                    ))}
-                {tab === "groups" && (
-                    <div className={styles.entries}>
-                        {mutualGroups.length === 0 ? (
-                            <div className={styles.empty}>
-                                <Text id="app.special.popovers.user_profile.no_groups" />
-                            </div>
-                        ) : (
-                            mutualGroups.map(
-                                x =>
-                                    x?.channel_type === "Group" && (
-                                        <Link to={`/channel/${x._id}`}>
-                                            <div
-                                                className={styles.entry}
-                                                key={x._id}
-                                            >
-                                                <ChannelIcon target={x} size={32} />
-                                                <span>{x.name}</span>
-                                            </div>
-                                        </Link>
-                                    )
-                            )
-                        )}
-                    </div>
-                )}
-            </div>
-        </Modal>
-    );
+	return (
+		<Modal
+			visible
+			border={dummy}
+			padding={false}
+			onClose={onClose}
+			dontModal={dummy}>
+			<div
+				className={styles.header}
+				data-force={profile?.background ? "light" : undefined}
+				style={{
+					backgroundImage:
+						backgroundURL &&
+						`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
+				}}>
+				<div className={styles.profile}>
+					<UserIcon size={80} target={user} status />
+					<div className={styles.details}>
+						<Localizer>
+							<span
+								className={styles.username}
+								onClick={() => writeClipboard(user.username)}>
+								@{user.username}
+							</span>
+						</Localizer>
+						{user.status?.text && (
+							<span className={styles.status}>
+								<UserStatus user={user} />
+							</span>
+						)}
+					</div>
+					{user.relationship === Users.Relationship.Friend && (
+						<Localizer>
+							<Tooltip
+								content={
+									<Text id="app.context_menu.message_user" />
+								}>
+								<IconButton
+									onClick={() => {
+										onClose();
+										history.push(`/open/${user_id}`);
+									}}>
+									<Envelope size={30} />
+								</IconButton>
+							</Tooltip>
+						</Localizer>
+					)}
+					{user.relationship === Users.Relationship.User && (
+						<IconButton
+							onClick={() => {
+								onClose();
+								if (dummy) return;
+								history.push(`/settings/profile`);
+							}}>
+							<Edit size={28} />
+						</IconButton>
+					)}
+					{(user.relationship === Users.Relationship.Incoming ||
+						user.relationship === Users.Relationship.None) && (
+						<IconButton
+							onClick={() =>
+								client.users.addFriend(user.username)
+							}>
+							<UserPlus size={28} />
+						</IconButton>
+					)}
+				</div>
+				<div className={styles.tabs}>
+					<div
+						data-active={tab === "profile"}
+						onClick={() => setTab("profile")}>
+						<Text id="app.special.popovers.user_profile.profile" />
+					</div>
+					{user.relationship !== Users.Relationship.User && (
+						<>
+							<div
+								data-active={tab === "friends"}
+								onClick={() => setTab("friends")}>
+								<Text id="app.special.popovers.user_profile.mutual_friends" />
+							</div>
+							<div
+								data-active={tab === "groups"}
+								onClick={() => setTab("groups")}>
+								<Text id="app.special.popovers.user_profile.mutual_groups" />
+							</div>
+						</>
+					)}
+				</div>
+			</div>
+			<div className={styles.content}>
+				{tab === "profile" && (
+					<div>
+						{!(profile?.content || badges > 0) && (
+							<div className={styles.empty}>
+								<Text id="app.special.popovers.user_profile.empty" />
+							</div>
+						)}
+						{badges > 0 && (
+							<div className={styles.category}>
+								<Text id="app.special.popovers.user_profile.sub.badges" />
+							</div>
+						)}
+						{badges > 0 && (
+							<div className={styles.badges}>
+								<Localizer>
+									{badges & Badges.Developer ? (
+										<Tooltip
+											content={
+												<Text id="app.navigation.tabs.dev" />
+											}>
+											<img src="/assets/badges/developer.svg" />
+										</Tooltip>
+									) : (
+										<></>
+									)}
+									{badges & Badges.Translator ? (
+										<Tooltip
+											content={
+												<Text id="app.special.popovers.user_profile.badges.translator" />
+											}>
+											<img src="/assets/badges/translator.svg" />
+										</Tooltip>
+									) : (
+										<></>
+									)}
+									{badges & Badges.EarlyAdopter ? (
+										<Tooltip
+											content={
+												<Text id="app.special.popovers.user_profile.badges.early_adopter" />
+											}>
+											<img src="/assets/badges/early_adopter.svg" />
+										</Tooltip>
+									) : (
+										<></>
+									)}
+									{badges & Badges.Supporter ? (
+										<Tooltip
+											content={
+												<Text id="app.special.popovers.user_profile.badges.supporter" />
+											}>
+											<Money size={32} color="#efab44" />
+										</Tooltip>
+									) : (
+										<></>
+									)}
+									{badges & Badges.ResponsibleDisclosure ? (
+										<Tooltip
+											content={
+												<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
+											}>
+											<Shield size={32} color="gray" />
+										</Tooltip>
+									) : (
+										<></>
+									)}
+								</Localizer>
+							</div>
+						)}
+						{profile?.content && (
+							<div className={styles.category}>
+								<Text id="app.special.popovers.user_profile.sub.information" />
+							</div>
+						)}
+						<Markdown content={profile?.content} />
+						{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
+					</div>
+				)}
+				{tab === "friends" &&
+					(users ? (
+						<div className={styles.entries}>
+							{users.length === 0 ? (
+								<div className={styles.empty}>
+									<Text id="app.special.popovers.user_profile.no_users" />
+								</div>
+							) : (
+								users.map(
+									(x) =>
+										x && (
+											<div
+												onClick={() =>
+													openScreen({
+														id: "profile",
+														user_id: x._id,
+													})
+												}
+												className={styles.entry}
+												key={x._id}>
+												<UserIcon
+													size={32}
+													target={x}
+												/>
+												<span>{x.username}</span>
+											</div>
+										),
+								)
+							)}
+						</div>
+					) : (
+						<Preloader type="ring" />
+					))}
+				{tab === "groups" && (
+					<div className={styles.entries}>
+						{mutualGroups.length === 0 ? (
+							<div className={styles.empty}>
+								<Text id="app.special.popovers.user_profile.no_groups" />
+							</div>
+						) : (
+							mutualGroups.map(
+								(x) =>
+									x?.channel_type === "Group" && (
+										<Link to={`/channel/${x._id}`}>
+											<div
+												className={styles.entry}
+												key={x._id}>
+												<ChannelIcon
+													target={x}
+													size={32}
+												/>
+												<span>{x.name}</span>
+											</div>
+										</Link>
+									),
+							)
+						)}
+					</div>
+				)}
+			</div>
+		</Modal>
+	);
 }
diff --git a/src/context/revoltjs/CheckAuth.tsx b/src/context/revoltjs/CheckAuth.tsx
index 1951985efed70317a1f87f1d24a26a260954d90e..0865a786a7e735d96f571ec68eff6fd9fd52c423 100644
--- a/src/context/revoltjs/CheckAuth.tsx
+++ b/src/context/revoltjs/CheckAuth.tsx
@@ -1,22 +1,23 @@
-import { useContext } from "preact/hooks";
 import { Redirect } from "react-router-dom";
-import { Children } from "../../types/Preact";
 
+import { useContext } from "preact/hooks";
+
+import { Children } from "../../types/Preact";
 import { OperationsContext } from "./RevoltClient";
 
 interface Props {
-    auth?: boolean;
-    children: Children;
+	auth?: boolean;
+	children: Children;
 }
 
 export const CheckAuth = (props: Props) => {
-    const operations = useContext(OperationsContext);
+	const operations = useContext(OperationsContext);
 
-    if (props.auth && !operations.ready()) {
-        return <Redirect to="/login" />;
-    } else if (!props.auth && operations.ready()) {
-        return <Redirect to="/" />;
-    }
+	if (props.auth && !operations.ready()) {
+		return <Redirect to="/login" />;
+	} else if (!props.auth && operations.ready()) {
+		return <Redirect to="/" />;
+	}
 
-    return <>{props.children}</>;
+	return <>{props.children}</>;
 };
diff --git a/src/context/revoltjs/FileUploads.tsx b/src/context/revoltjs/FileUploads.tsx
index 87781e2f2ef9cb78b2c59dc995010a693446db34..14b55f2e2b7ceeed454732386108032d00f99236 100644
--- a/src/context/revoltjs/FileUploads.tsx
+++ b/src/context/revoltjs/FileUploads.tsx
@@ -1,221 +1,292 @@
-import { Text } from "preact-i18n";
-import { takeError } from "./util";
-import classNames from "classnames";
-import { AppContext } from "./RevoltClient";
-import styles from './FileUploads.module.scss';
+import { Plus, X, XCircle } from "@styled-icons/boxicons-regular";
+import { Pencil } from "@styled-icons/boxicons-solid";
 import Axios, { AxiosRequestConfig } from "axios";
+
+import styles from "./FileUploads.module.scss";
+import classNames from "classnames";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
-import Preloader from "../../components/ui/Preloader";
+
 import { determineFileSize } from "../../lib/fileSize";
-import IconButton from '../../components/ui/IconButton';
-import { Plus, X, XCircle } from "@styled-icons/boxicons-regular";
-import { Pencil } from "@styled-icons/boxicons-solid";
+
+import IconButton from "../../components/ui/IconButton";
+import Preloader from "../../components/ui/Preloader";
+
 import { useIntermediate } from "../intermediate/Intermediate";
+import { AppContext } from "./RevoltClient";
+import { takeError } from "./util";
 
 type Props = {
-    maxFileSize: number
-    remove: () => Promise<void>
-    fileType: 'backgrounds' | 'icons' | 'avatars' | 'attachments' | 'banners'
+	maxFileSize: number;
+	remove: () => Promise<void>;
+	fileType: "backgrounds" | "icons" | "avatars" | "attachments" | "banners";
 } & (
-    { behaviour: 'ask', onChange: (file: File) => void } |
-    { behaviour: 'upload', onUpload: (id: string) => Promise<void> } |
-    { behaviour: 'multi', onChange: (files: File[]) => void, append?: (files: File[]) => void }
-) & (
-    { style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } |
-    { style: 'attachment', attached: boolean, uploading: boolean, cancel: () => void, size?: number }
-)
-
-export async function uploadFile(autumnURL: string, tag: string, file: File, config?: AxiosRequestConfig) {
-    const formData = new FormData();
-    formData.append("file", file);
-    
-    const res = await Axios.post(autumnURL + "/" + tag, formData, {
-        headers: {
-            "Content-Type": "multipart/form-data"
-        },
-        ...config
-    });
-
-    return res.data.id;
+	| { behaviour: "ask"; onChange: (file: File) => void }
+	| { behaviour: "upload"; onUpload: (id: string) => Promise<void> }
+	| {
+			behaviour: "multi";
+			onChange: (files: File[]) => void;
+			append?: (files: File[]) => void;
+	  }
+) &
+	(
+		| {
+				style: "icon" | "banner";
+				defaultPreview?: string;
+				previewURL?: string;
+				width?: number;
+				height?: number;
+		  }
+		| {
+				style: "attachment";
+				attached: boolean;
+				uploading: boolean;
+				cancel: () => void;
+				size?: number;
+		  }
+	);
+
+export async function uploadFile(
+	autumnURL: string,
+	tag: string,
+	file: File,
+	config?: AxiosRequestConfig,
+) {
+	const formData = new FormData();
+	formData.append("file", file);
+
+	const res = await Axios.post(autumnURL + "/" + tag, formData, {
+		headers: {
+			"Content-Type": "multipart/form-data",
+		},
+		...config,
+	});
+
+	return res.data.id;
 }
 
-export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooLarge: () => void, multiple?: boolean) {
-    const input = document.createElement("input");
-    input.type = "file";
-    input.multiple = multiple ?? false;
+export function grabFiles(
+	maxFileSize: number,
+	cb: (files: File[]) => void,
+	tooLarge: () => void,
+	multiple?: boolean,
+) {
+	const input = document.createElement("input");
+	input.type = "file";
+	input.multiple = multiple ?? false;
 
-    input.onchange = async (e) => {
-        const files = (e.currentTarget as HTMLInputElement)?.files;
-        if (!files) return;
-        for (let file of files) {
-            if (file.size > maxFileSize) {
-                return tooLarge();
-            }
-        }
+	input.onchange = async (e) => {
+		const files = (e.currentTarget as HTMLInputElement)?.files;
+		if (!files) return;
+		for (let file of files) {
+			if (file.size > maxFileSize) {
+				return tooLarge();
+			}
+		}
 
-        cb(Array.from(files));
-    };
+		cb(Array.from(files));
+	};
 
-    input.click();
+	input.click();
 }
 
 export function FileUploader(props: Props) {
-    const { fileType, maxFileSize, remove } = props;
-    const { openScreen } = useIntermediate();
-    const client = useContext(AppContext);
-
-    const [ uploading, setUploading ] = useState(false);
-
-    function onClick() {
-        if (uploading) return;
-
-        grabFiles(maxFileSize, async files => {
-            setUploading(true);
-
-            try {
-                if (props.behaviour === 'multi') {
-                    props.onChange(files);
-                } else if (props.behaviour === 'ask') {
-                    props.onChange(files[0]);
-                } else {
-                    await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, files[0]));
-                }
-            } catch (err) {
-                return openScreen({ id: "error", error: takeError(err) });
-            } finally {
-                setUploading(false);
-            }
-        }, () =>
-            openScreen({ id: "error", error: "FileTooLarge" }),
-            props.behaviour === 'multi');
-    }
-
-    function removeOrUpload() {
-        if (uploading) return;
-
-        if (props.style === 'attachment') {
-            if (props.attached) {
-                props.remove();
-            } else {
-                onClick();
-            }
-        } else {
-            if (props.previewURL) {
-                props.remove();
-            } else {
-                onClick();
-            }
-        }
-    }
-
-    if (props.behaviour === 'multi' && props.append) {
-        useEffect(() => {
-            // File pasting.
-            function paste(e: ClipboardEvent) {
-                const items = e.clipboardData?.items;
-                if (typeof items === "undefined") return;
-                if (props.behaviour !== 'multi' || !props.append) return;
-
-                let files = [];
-                for (const item of items) {
-                    if (!item.type.startsWith("text/")) {
-                        const blob = item.getAsFile();
-                        if (blob) {
-                            if (blob.size > props.maxFileSize) {
-                                openScreen({ id: 'error', error: 'FileTooLarge' });
-                            }
-
-                            files.push(blob);
-                        }
-                    }
-                }
-
-                props.append(files);
-            }
-
-            // Let the browser know we can drop files.
-            function dragover(e: DragEvent) {
-                e.stopPropagation();
-                e.preventDefault();
-                if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
-            }
-
-            // File dropping.
-            function drop(e: DragEvent) {
-                e.preventDefault();
-                if (props.behaviour !== 'multi' || !props.append) return;
-
-                const dropped = e.dataTransfer?.files;
-                if (dropped) {
-                    let files = [];
-                    for (const item of dropped) {
-                        if (item.size > props.maxFileSize) {
-                            openScreen({ id: 'error', error: 'FileTooLarge' });
-                        }
-
-                        files.push(item);
-                    }
-
-                    props.append(files);
-                }
-            }
-
-            document.addEventListener("paste", paste);
-            document.addEventListener("dragover", dragover);
-            document.addEventListener("drop", drop);
-
-            return () => {
-                document.removeEventListener("paste", paste);
-                document.removeEventListener("dragover", dragover);
-                document.removeEventListener("drop", drop);
-            };
-        }, [ props.append ]);
-    }
-
-    if (props.style === 'icon' || props.style === 'banner') {
-        const { style, previewURL, defaultPreview, width, height } = props;
-        return (
-            <div className={classNames(styles.uploader,
-                { [styles.icon]: style === 'icon',
-                [styles.banner]: style === 'banner' })}
-                data-uploading={uploading}>
-                <div className={styles.image}
-                    style={{ backgroundImage:
-                        style === 'icon' ? `url('${previewURL ?? defaultPreview}')` :
-                        (previewURL ? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')` : 'black'),
-                        width, height
-                    }}
-                    onClick={onClick}>
-                    { uploading ?
-                        <div className={styles.uploading}>
-                            <Preloader type="ring" />
-                        </div> :
-                        <div className={styles.edit}>
-                            <Pencil size={30} />
-                        </div> }
-                </div>
-                <div className={styles.modify}>
-                    <span onClick={removeOrUpload}>{ 
-                        uploading ? <Text id="app.main.channel.uploading_file" /> :
-                        props.previewURL ? <Text id="app.settings.actions.remove" /> :
-                        <Text id="app.settings.actions.upload" /> }</span>
-                    <span className={styles.small}><Text id="app.settings.actions.max_filesize" fields={{ filesize: determineFileSize(maxFileSize) }} /></span>
-                </div>
-            </div>
-        )
-    } else if (props.style === 'attachment') {
-        const { attached, uploading, cancel, size } = props;
-        return (
-            <IconButton
-                onClick={() => {
-                    if (uploading) return cancel();
-                    if (attached) return remove();
-                    onClick();
-                }}>
-                { uploading ? <XCircle size={size} /> : attached ? <X size={size} /> : <Plus size={size} />}
-            </IconButton>
-        )
-    }
-
-    return null;
+	const { fileType, maxFileSize, remove } = props;
+	const { openScreen } = useIntermediate();
+	const client = useContext(AppContext);
+
+	const [uploading, setUploading] = useState(false);
+
+	function onClick() {
+		if (uploading) return;
+
+		grabFiles(
+			maxFileSize,
+			async (files) => {
+				setUploading(true);
+
+				try {
+					if (props.behaviour === "multi") {
+						props.onChange(files);
+					} else if (props.behaviour === "ask") {
+						props.onChange(files[0]);
+					} else {
+						await props.onUpload(
+							await uploadFile(
+								client.configuration!.features.autumn.url,
+								fileType,
+								files[0],
+							),
+						);
+					}
+				} catch (err) {
+					return openScreen({ id: "error", error: takeError(err) });
+				} finally {
+					setUploading(false);
+				}
+			},
+			() => openScreen({ id: "error", error: "FileTooLarge" }),
+			props.behaviour === "multi",
+		);
+	}
+
+	function removeOrUpload() {
+		if (uploading) return;
+
+		if (props.style === "attachment") {
+			if (props.attached) {
+				props.remove();
+			} else {
+				onClick();
+			}
+		} else {
+			if (props.previewURL) {
+				props.remove();
+			} else {
+				onClick();
+			}
+		}
+	}
+
+	if (props.behaviour === "multi" && props.append) {
+		useEffect(() => {
+			// File pasting.
+			function paste(e: ClipboardEvent) {
+				const items = e.clipboardData?.items;
+				if (typeof items === "undefined") return;
+				if (props.behaviour !== "multi" || !props.append) return;
+
+				let files = [];
+				for (const item of items) {
+					if (!item.type.startsWith("text/")) {
+						const blob = item.getAsFile();
+						if (blob) {
+							if (blob.size > props.maxFileSize) {
+								openScreen({
+									id: "error",
+									error: "FileTooLarge",
+								});
+							}
+
+							files.push(blob);
+						}
+					}
+				}
+
+				props.append(files);
+			}
+
+			// Let the browser know we can drop files.
+			function dragover(e: DragEvent) {
+				e.stopPropagation();
+				e.preventDefault();
+				if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
+			}
+
+			// File dropping.
+			function drop(e: DragEvent) {
+				e.preventDefault();
+				if (props.behaviour !== "multi" || !props.append) return;
+
+				const dropped = e.dataTransfer?.files;
+				if (dropped) {
+					let files = [];
+					for (const item of dropped) {
+						if (item.size > props.maxFileSize) {
+							openScreen({ id: "error", error: "FileTooLarge" });
+						}
+
+						files.push(item);
+					}
+
+					props.append(files);
+				}
+			}
+
+			document.addEventListener("paste", paste);
+			document.addEventListener("dragover", dragover);
+			document.addEventListener("drop", drop);
+
+			return () => {
+				document.removeEventListener("paste", paste);
+				document.removeEventListener("dragover", dragover);
+				document.removeEventListener("drop", drop);
+			};
+		}, [props.append]);
+	}
+
+	if (props.style === "icon" || props.style === "banner") {
+		const { style, previewURL, defaultPreview, width, height } = props;
+		return (
+			<div
+				className={classNames(styles.uploader, {
+					[styles.icon]: style === "icon",
+					[styles.banner]: style === "banner",
+				})}
+				data-uploading={uploading}>
+				<div
+					className={styles.image}
+					style={{
+						backgroundImage:
+							style === "icon"
+								? `url('${previewURL ?? defaultPreview}')`
+								: previewURL
+								? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')`
+								: "black",
+						width,
+						height,
+					}}
+					onClick={onClick}>
+					{uploading ? (
+						<div className={styles.uploading}>
+							<Preloader type="ring" />
+						</div>
+					) : (
+						<div className={styles.edit}>
+							<Pencil size={30} />
+						</div>
+					)}
+				</div>
+				<div className={styles.modify}>
+					<span onClick={removeOrUpload}>
+						{uploading ? (
+							<Text id="app.main.channel.uploading_file" />
+						) : props.previewURL ? (
+							<Text id="app.settings.actions.remove" />
+						) : (
+							<Text id="app.settings.actions.upload" />
+						)}
+					</span>
+					<span className={styles.small}>
+						<Text
+							id="app.settings.actions.max_filesize"
+							fields={{
+								filesize: determineFileSize(maxFileSize),
+							}}
+						/>
+					</span>
+				</div>
+			</div>
+		);
+	} else if (props.style === "attachment") {
+		const { attached, uploading, cancel, size } = props;
+		return (
+			<IconButton
+				onClick={() => {
+					if (uploading) return cancel();
+					if (attached) return remove();
+					onClick();
+				}}>
+				{uploading ? (
+					<XCircle size={size} />
+				) : attached ? (
+					<X size={size} />
+				) : (
+					<Plus size={size} />
+				)}
+			</IconButton>
+		);
+	}
+
+	return null;
 }
diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx
index 4ae6c74a61da7fd3fcf025d3c8aa0ff020847673..66e03059cacd44885eef9e0d27e839b3fba95859 100644
--- a/src/context/revoltjs/Notifications.tsx
+++ b/src/context/revoltjs/Notifications.tsx
@@ -1,247 +1,286 @@
-import { decodeTime } from "ulid";
-import { SoundContext } from "../Settings";
-import { AppContext } from "./RevoltClient";
-import { useTranslation } from "../../lib/i18n";
+import { Route, Switch, useHistory, useParams } from "react-router-dom";
+import { Message, SYSTEM_USER_ID, User } from "revolt.js";
 import { Users } from "revolt.js/dist/api/objects";
+import { decodeTime } from "ulid";
+
 import { useContext, useEffect } from "preact/hooks";
+
+import { useTranslation } from "../../lib/i18n";
+
 import { connectState } from "../../redux/connector";
-import { Message, SYSTEM_USER_ID, User } from "revolt.js";
+import {
+	getNotificationState,
+	Notifications,
+	shouldNotify,
+} from "../../redux/reducers/notifications";
 import { NotificationOptions } from "../../redux/reducers/settings";
-import { Route, Switch, useHistory, useParams } from "react-router-dom";
-import { getNotificationState, Notifications, shouldNotify } from "../../redux/reducers/notifications";
+
+import { SoundContext } from "../Settings";
+import { AppContext } from "./RevoltClient";
 
 interface Props {
-    options?: NotificationOptions;
-    notifs: Notifications;
+	options?: NotificationOptions;
+	notifs: Notifications;
 }
 
 const notifications: { [key: string]: Notification } = {};
 
-async function createNotification(title: string, options: globalThis.NotificationOptions) {
-    try {
-        return new Notification(title, options);
-    } catch (err) {
-        let sw = await navigator.serviceWorker.getRegistration();
-        sw?.showNotification(title, options);
-    }
+async function createNotification(
+	title: string,
+	options: globalThis.NotificationOptions,
+) {
+	try {
+		return new Notification(title, options);
+	} catch (err) {
+		let sw = await navigator.serviceWorker.getRegistration();
+		sw?.showNotification(title, options);
+	}
 }
 
 function Notifier({ options, notifs }: Props) {
-    const translate = useTranslation();
-    const showNotification = options?.desktopEnabled ?? false;
-
-    const client = useContext(AppContext);
-    const { guild: guild_id, channel: channel_id } = useParams<{
-        guild: string;
-        channel: string;
-    }>();
-    const history = useHistory();
-    const playSound = useContext(SoundContext);
-
-    async function message(msg: Message) {
-        if (msg.author === client.user!._id) return;
-        if (msg.channel === channel_id && document.hasFocus()) return;
-        if (client.user!.status?.presence === Users.Presence.Busy) return;
-
-        const channel = client.channels.get(msg.channel);
-        const author = client.users.get(msg.author);
-        if (!channel) return;
-        if (author?.relationship === Users.Relationship.Blocked) return;
-
-        const notifState = getNotificationState(notifs, channel);
-        if (!shouldNotify(notifState, msg, client.user!._id)) return;
-
-        playSound('message');
-        if (!showNotification) return;
-
-        let title;
-        switch (channel.channel_type) {
-            case "SavedMessages":
-                return;
-            case "DirectMessage":
-                title = `@${author?.username}`;
-                break;
-            case "Group":
-                if (author?._id === SYSTEM_USER_ID) {
-                    title = channel.name;
-                } else {
-                    title = `@${author?.username} - ${channel.name}`;
-                }
-                break;
-            case "TextChannel":
-                const server = client.servers.get(channel.server);
-                title = `@${author?.username} (#${channel.name}, ${server?.name})`;
-                break;
-            default:
-                title = msg.channel;
-                break;
-        }
-
-        let image;
-        if (msg.attachments) {
-            let imageAttachment = msg.attachments.find(x => x.metadata.type === 'Image');
-            if (imageAttachment) {
-                image = client.generateFileURL(imageAttachment, { max_side: 720 });
-            }
-        }
-
-        let body, icon;
-        if (typeof msg.content === "string") {
-            body = client.markdownToText(msg.content);
-            icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
-        } else {
-            let users = client.users;
-            switch (msg.content.type) {
-                case "user_added":
-                case "user_remove":
-                    body = translate(
-                        `app.main.channel.system.${msg.content.type === 'user_added' ? 'added_by' : 'removed_by'}`,
-                        { user: users.get(msg.content.id)?.username, other_user: users.get(msg.content.by)?.username }
-                    );
-                    icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
-                    break;
-                case "user_joined":
-                case "user_left":
-                case "user_kicked":
-                case "user_banned":
-                    body = translate(
-                        `app.main.channel.system.${msg.content.type}`,
-                        { user: users.get(msg.content.id)?.username }
-                    );
-                    icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
-                    break;
-                case "channel_renamed":
-                    body = translate(
-                        `app.main.channel.system.channel_renamed`,
-                        { user: users.get(msg.content.by)?.username, name: msg.content.name }
-                    );
-                    icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
-                    break;
-                case "channel_description_changed":
-                case "channel_icon_changed":
-                    body = translate(
-                        `app.main.channel.system.${msg.content.type}`,
-                        { user: users.get(msg.content.by)?.username }
-                    );
-                    icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
-                    break;
-            }
-        }
-
-        let notif = await createNotification(title, {
-            icon,
-            image,
-            body,
-            timestamp: decodeTime(msg._id),
-            tag: msg.channel,
-            badge: '/assets/icons/android-chrome-512x512.png',
-            silent: true
-        });
-
-        if (notif) {
-            notif.addEventListener("click", () => {
-                window.focus();
-                const id = msg.channel;
-                if (id !== channel_id) {
-                    let channel = client.channels.get(id);
-                    if (channel) {
-                        if (channel.channel_type === 'TextChannel') {
-                            history.push(`/server/${channel.server}/channel/${id}`);
-                        } else {
-                            history.push(`/channel/${id}`);
-                        }
-                    }
-                }
-            });
-
-            notifications[msg.channel] = notif;
-            notif.addEventListener(
-                "close",
-                () => delete notifications[msg.channel]
-            );
-        }
-    }
-
-    async function relationship(user: User, property: string) {
-        if (client.user?.status?.presence === Users.Presence.Busy) return;
-        if (property !== "relationship") return;
-        if (!showNotification) return;
-
-        let event;
-        switch (user.relationship) {
-            case Users.Relationship.Incoming:
-                event = translate("notifications.sent_request", { person: user.username });
-                break;
-            case Users.Relationship.Friend:
-                event = translate("notifications.now_friends", { person: user.username });
-                break;
-            default:
-                return;
-        }
-
-        let notif = await createNotification(event, {
-            icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
-            badge: '/assets/icons/android-chrome-512x512.png',
-            timestamp: +new Date()
-        });
-
-        notif?.addEventListener("click", () => {
-            history.push(`/friends`);
-        });
-    }
-
-    useEffect(() => {
-        client.addListener("message", message);
-        client.users.addListener("mutation", relationship);
-
-        return () => {
-            client.removeListener("message", message);
-            client.users.removeListener("mutation", relationship);
-        };
-    }, [client, playSound, guild_id, channel_id, showNotification, notifs]);
-
-    useEffect(() => {
-        function visChange() {
-            if (document.visibilityState === "visible") {
-                if (notifications[channel_id]) {
-                    notifications[channel_id].close();
-                }
-            }
-        }
-
-        visChange();
-
-        document.addEventListener("visibilitychange", visChange);
-        return () =>
-            document.removeEventListener("visibilitychange", visChange);
-    }, [guild_id, channel_id]);
-
-    return null;
+	const translate = useTranslation();
+	const showNotification = options?.desktopEnabled ?? false;
+
+	const client = useContext(AppContext);
+	const { guild: guild_id, channel: channel_id } = useParams<{
+		guild: string;
+		channel: string;
+	}>();
+	const history = useHistory();
+	const playSound = useContext(SoundContext);
+
+	async function message(msg: Message) {
+		if (msg.author === client.user!._id) return;
+		if (msg.channel === channel_id && document.hasFocus()) return;
+		if (client.user!.status?.presence === Users.Presence.Busy) return;
+
+		const channel = client.channels.get(msg.channel);
+		const author = client.users.get(msg.author);
+		if (!channel) return;
+		if (author?.relationship === Users.Relationship.Blocked) return;
+
+		const notifState = getNotificationState(notifs, channel);
+		if (!shouldNotify(notifState, msg, client.user!._id)) return;
+
+		playSound("message");
+		if (!showNotification) return;
+
+		let title;
+		switch (channel.channel_type) {
+			case "SavedMessages":
+				return;
+			case "DirectMessage":
+				title = `@${author?.username}`;
+				break;
+			case "Group":
+				if (author?._id === SYSTEM_USER_ID) {
+					title = channel.name;
+				} else {
+					title = `@${author?.username} - ${channel.name}`;
+				}
+				break;
+			case "TextChannel":
+				const server = client.servers.get(channel.server);
+				title = `@${author?.username} (#${channel.name}, ${server?.name})`;
+				break;
+			default:
+				title = msg.channel;
+				break;
+		}
+
+		let image;
+		if (msg.attachments) {
+			let imageAttachment = msg.attachments.find(
+				(x) => x.metadata.type === "Image",
+			);
+			if (imageAttachment) {
+				image = client.generateFileURL(imageAttachment, {
+					max_side: 720,
+				});
+			}
+		}
+
+		let body, icon;
+		if (typeof msg.content === "string") {
+			body = client.markdownToText(msg.content);
+			icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
+		} else {
+			let users = client.users;
+			switch (msg.content.type) {
+				case "user_added":
+				case "user_remove":
+					body = translate(
+						`app.main.channel.system.${
+							msg.content.type === "user_added"
+								? "added_by"
+								: "removed_by"
+						}`,
+						{
+							user: users.get(msg.content.id)?.username,
+							other_user: users.get(msg.content.by)?.username,
+						},
+					);
+					icon = client.users.getAvatarURL(msg.content.id, {
+						max_side: 256,
+					});
+					break;
+				case "user_joined":
+				case "user_left":
+				case "user_kicked":
+				case "user_banned":
+					body = translate(
+						`app.main.channel.system.${msg.content.type}`,
+						{ user: users.get(msg.content.id)?.username },
+					);
+					icon = client.users.getAvatarURL(msg.content.id, {
+						max_side: 256,
+					});
+					break;
+				case "channel_renamed":
+					body = translate(
+						`app.main.channel.system.channel_renamed`,
+						{
+							user: users.get(msg.content.by)?.username,
+							name: msg.content.name,
+						},
+					);
+					icon = client.users.getAvatarURL(msg.content.by, {
+						max_side: 256,
+					});
+					break;
+				case "channel_description_changed":
+				case "channel_icon_changed":
+					body = translate(
+						`app.main.channel.system.${msg.content.type}`,
+						{ user: users.get(msg.content.by)?.username },
+					);
+					icon = client.users.getAvatarURL(msg.content.by, {
+						max_side: 256,
+					});
+					break;
+			}
+		}
+
+		let notif = await createNotification(title, {
+			icon,
+			image,
+			body,
+			timestamp: decodeTime(msg._id),
+			tag: msg.channel,
+			badge: "/assets/icons/android-chrome-512x512.png",
+			silent: true,
+		});
+
+		if (notif) {
+			notif.addEventListener("click", () => {
+				window.focus();
+				const id = msg.channel;
+				if (id !== channel_id) {
+					let channel = client.channels.get(id);
+					if (channel) {
+						if (channel.channel_type === "TextChannel") {
+							history.push(
+								`/server/${channel.server}/channel/${id}`,
+							);
+						} else {
+							history.push(`/channel/${id}`);
+						}
+					}
+				}
+			});
+
+			notifications[msg.channel] = notif;
+			notif.addEventListener(
+				"close",
+				() => delete notifications[msg.channel],
+			);
+		}
+	}
+
+	async function relationship(user: User, property: string) {
+		if (client.user?.status?.presence === Users.Presence.Busy) return;
+		if (property !== "relationship") return;
+		if (!showNotification) return;
+
+		let event;
+		switch (user.relationship) {
+			case Users.Relationship.Incoming:
+				event = translate("notifications.sent_request", {
+					person: user.username,
+				});
+				break;
+			case Users.Relationship.Friend:
+				event = translate("notifications.now_friends", {
+					person: user.username,
+				});
+				break;
+			default:
+				return;
+		}
+
+		let notif = await createNotification(event, {
+			icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
+			badge: "/assets/icons/android-chrome-512x512.png",
+			timestamp: +new Date(),
+		});
+
+		notif?.addEventListener("click", () => {
+			history.push(`/friends`);
+		});
+	}
+
+	useEffect(() => {
+		client.addListener("message", message);
+		client.users.addListener("mutation", relationship);
+
+		return () => {
+			client.removeListener("message", message);
+			client.users.removeListener("mutation", relationship);
+		};
+	}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
+
+	useEffect(() => {
+		function visChange() {
+			if (document.visibilityState === "visible") {
+				if (notifications[channel_id]) {
+					notifications[channel_id].close();
+				}
+			}
+		}
+
+		visChange();
+
+		document.addEventListener("visibilitychange", visChange);
+		return () =>
+			document.removeEventListener("visibilitychange", visChange);
+	}, [guild_id, channel_id]);
+
+	return null;
 }
 
 const NotifierComponent = connectState(
-    Notifier,
-    state => {
-        return {
-            options: state.settings.notification,
-            notifs: state.notifications
-        };
-    },
-    true
+	Notifier,
+	(state) => {
+		return {
+			options: state.settings.notification,
+			notifs: state.notifications,
+		};
+	},
+	true,
 );
 
-export default function Notifications() {
-    return (
-        <Switch>
-            <Route path="/server/:server/channel/:channel">
-                <NotifierComponent />
-            </Route>
-            <Route path="/channel/:channel">
-                <NotifierComponent />
-            </Route>
-            <Route path="/">
-                <NotifierComponent />
-            </Route>
-        </Switch>
-    );
+export default function NotificationsComponent() {
+	return (
+		<Switch>
+			<Route path="/server/:server/channel/:channel">
+				<NotifierComponent />
+			</Route>
+			<Route path="/channel/:channel">
+				<NotifierComponent />
+			</Route>
+			<Route path="/">
+				<NotifierComponent />
+			</Route>
+		</Switch>
+	);
 }
diff --git a/src/context/revoltjs/RequiresOnline.tsx b/src/context/revoltjs/RequiresOnline.tsx
index ff2dd19998d6201e13acaf366bcb031d0864c479..a68ce45901e4a01efd5e56705de5bc52daa62ae6 100644
--- a/src/context/revoltjs/RequiresOnline.tsx
+++ b/src/context/revoltjs/RequiresOnline.tsx
@@ -1,44 +1,47 @@
-import { Text } from "preact-i18n";
+import { WifiOff } from "@styled-icons/boxicons-regular";
 import styled from "styled-components";
+
+import { Text } from "preact-i18n";
 import { useContext } from "preact/hooks";
-import { Children } from "../../types/Preact";
-import { WifiOff } from "@styled-icons/boxicons-regular";
+
 import Preloader from "../../components/ui/Preloader";
+
+import { Children } from "../../types/Preact";
 import { ClientStatus, StatusContext } from "./RevoltClient";
 
 interface Props {
-    children: Children;
+	children: Children;
 }
 
 const Base = styled.div`
-    gap: 16px;
-    padding: 1em;
-    display: flex;
-    user-select: none;
-    align-items: center;
-    flex-direction: row;
-    justify-content: center;
-    color: var(--tertiary-foreground);
-    background: var(--secondary-header);
-
-    > div {
-        font-size: 18px;
-    }
+	gap: 16px;
+	padding: 1em;
+	display: flex;
+	user-select: none;
+	align-items: center;
+	flex-direction: row;
+	justify-content: center;
+	color: var(--tertiary-foreground);
+	background: var(--secondary-header);
+
+	> div {
+		font-size: 18px;
+	}
 `;
 
 export default function RequiresOnline(props: Props) {
-    const status = useContext(StatusContext);
-
-    if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
-    if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
-        return (
-            <Base>
-                <WifiOff size={16} />
-                <div>
-                    <Text id="app.special.requires_online" />
-                </div>
-            </Base>
-        );
-
-    return <>{ props.children }</>;
+	const status = useContext(StatusContext);
+
+	if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
+	if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
+		return (
+			<Base>
+				<WifiOff size={16} />
+				<div>
+					<Text id="app.special.requires_online" />
+				</div>
+			</Base>
+		);
+
+	return <>{props.children}</>;
 }
diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx
index 91dc602f0bdc2724b4b55fdf271b6a4fe4edc0d4..ddf67a19c29283762cd19b1d3f3a8c0fe57473b1 100644
--- a/src/context/revoltjs/RevoltClient.tsx
+++ b/src/context/revoltjs/RevoltClient.tsx
@@ -1,37 +1,42 @@
-import { openDB } from 'idb';
+import { openDB } from "idb";
+import { useHistory } from "react-router-dom";
 import { Client } from "revolt.js";
-import { takeError } from "./util";
-import { createContext } from "preact";
-import { dispatch } from '../../redux';
-import { Children } from "../../types/Preact";
-import { useHistory } from 'react-router-dom';
 import { Route } from "revolt.js/dist/api/routes";
+
+import { createContext } from "preact";
+import { useEffect, useMemo, useState } from "preact/hooks";
+
+import { SingletonMessageRenderer } from "../../lib/renderer/Singleton";
+
+import { dispatch } from "../../redux";
 import { connectState } from "../../redux/connector";
-import Preloader from "../../components/ui/Preloader";
 import { AuthState } from "../../redux/reducers/auth";
-import { useEffect, useMemo, useState } from "preact/hooks";
-import { useIntermediate } from '../intermediate/Intermediate';
+
+import Preloader from "../../components/ui/Preloader";
+
+import { Children } from "../../types/Preact";
+import { useIntermediate } from "../intermediate/Intermediate";
 import { registerEvents, setReconnectDisallowed } from "./events";
-import { SingletonMessageRenderer } from '../../lib/renderer/Singleton';
+import { takeError } from "./util";
 
 export enum ClientStatus {
-    INIT,
-    LOADING,
-    READY,
-    OFFLINE,
-    DISCONNECTED,
-    CONNECTING,
-    RECONNECTING,
-    ONLINE,
+	INIT,
+	LOADING,
+	READY,
+	OFFLINE,
+	DISCONNECTED,
+	CONNECTING,
+	RECONNECTING,
+	ONLINE,
 }
 
 export interface ClientOperations {
-    login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
-    logout: (shouldRequest?: boolean) => Promise<void>;
-    loggedIn: () => boolean;
-    ready: () => boolean;
+	login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
+	logout: (shouldRequest?: boolean) => Promise<void>;
+	loggedIn: () => boolean;
+	ready: () => boolean;
 
-    openDM: (user_id: string) => Promise<string>;
+	openDM: (user_id: string) => Promise<string>;
 }
 
 // By the time they are used, they should all be initialized.
@@ -42,189 +47,195 @@ export const StatusContext = createContext<ClientStatus>(null!);
 export const OperationsContext = createContext<ClientOperations>(null!);
 
 type Props = {
-    auth: AuthState;
-    children: Children;
+	auth: AuthState;
+	children: Children;
 };
 
 function Context({ auth, children }: Props) {
-    const history = useHistory();
-    const { openScreen } = useIntermediate();
-    const [status, setStatus] = useState(ClientStatus.INIT);
-    const [client, setClient] = useState<Client>(undefined as unknown as Client);
-
-    useEffect(() => {
-        (async () => {
-            let db;
-            try {
-                // Match sw.ts#L23
-                db = await openDB('state', 3, {
-                    upgrade(db) {
-                        for (let store of [ "channels", "servers", "users", "members" ]) {
-                            db.createObjectStore(store, {
-                                keyPath: '_id'
-                            });
-                        }
-                    },
-                });
-            } catch (err) {
-                console.error('Failed to open IndexedDB store, continuing without.');
-            }
-
-            const client = new Client({
-                autoReconnect: false,
-                apiURL: import.meta.env.VITE_API_URL,
-                debug: import.meta.env.DEV,
-                db
-            });
-
-            setClient(client);
-            SingletonMessageRenderer.subscribe(client);
-            setStatus(ClientStatus.LOADING);
-        })();
-    }, [ ]);
-
-    if (status === ClientStatus.INIT) return null;
-
-    const operations: ClientOperations = useMemo(() => {
-        return {
-            login: async data => {
-                setReconnectDisallowed(true);
-
-                try {
-                    const onboarding = await client.login(data);
-                    setReconnectDisallowed(false);
-                    const login = () =>
-                        dispatch({
-                            type: "LOGIN",
-                            session: client.session! // This [null assertion] is ok, we should have a session by now. - insert's words
-                        });
-
-                    if (onboarding) {
-                        openScreen({
-                            id: "onboarding",
-                            callback: (username: string) =>
-                                onboarding(username, true).then(login)
-                        });
-                    } else {
-                        login();
-                    }
-                } catch (err) {
-                    setReconnectDisallowed(false);
-                    throw err;
-                }
-            },
-            logout: async shouldRequest => {
-                dispatch({ type: "LOGOUT" });
-
-                client.reset();
-                dispatch({ type: "RESET" });
-
-                openScreen({ id: "none" });
-                setStatus(ClientStatus.READY);
-
-                client.websocket.disconnect();
-
-                if (shouldRequest) {
-                    try {
-                        await client.logout();
-                    } catch (err) {
-                        console.error(err);
-                    }
-                }
-            },
-            loggedIn: () => typeof auth.active !== "undefined",
-            ready: () => (
-                operations.loggedIn() &&
-                typeof client.user !== "undefined"
-            ),
-            openDM: async (user_id: string) => {
-                let channel = await client.users.openDM(user_id);
-                history.push(`/channel/${channel!._id}`);
-                return channel!._id;
-            }
-        }
-    }, [ client, auth.active ]);
-
-    useEffect(() => registerEvents({ operations }, setStatus, client), [ client ]);
-
-    useEffect(() => {
-        (async () => {
-            if (client.db) {
-                await client.restore();
-            }
-
-            if (auth.active) {
-                dispatch({ type: "QUEUE_FAIL_ALL" });
-
-                const active = auth.accounts[auth.active];
-                client.user = client.users.get(active.session.user_id);
-                if (!navigator.onLine) {
-                    return setStatus(ClientStatus.OFFLINE);
-                }
-
-                if (operations.ready())
-                    setStatus(ClientStatus.CONNECTING);
-                
-                if (navigator.onLine) {
-                    await client
-                        .fetchConfiguration()
-                        .catch(() =>
-                            console.error("Failed to connect to API server.")
-                        );
-                }
-
-                try {
-                    await client.fetchConfiguration();
-                    const callback = await client.useExistingSession(
-                        active.session
-                    );
-
-                    if (callback) {
-                        openScreen({ id: "onboarding", callback });
-                    }
-                } catch (err) {
-                    setStatus(ClientStatus.DISCONNECTED);
-                    const error = takeError(err);
-                    if (error === "Forbidden" || error === "Unauthorized") {
-                        operations.logout(true);
-                        openScreen({ id: "signed_out" });
-                    } else {
-                        openScreen({ id: "error", error });
-                    }
-                }
-            } else {
-                try {
-                    await client.fetchConfiguration()
-                } catch (err) {
-                    console.error("Failed to connect to API server.");
-                }
-
-                setStatus(ClientStatus.READY);
-            }
-        })();
-    }, []);
-
-    if (status === ClientStatus.LOADING) {
-        return <Preloader type="spinner" />;
-    }
-
-    return (
-        <AppContext.Provider value={client}>
-            <StatusContext.Provider value={status}>
-                <OperationsContext.Provider value={operations}>
-                    { children }
-                </OperationsContext.Provider>
-            </StatusContext.Provider>
-        </AppContext.Provider>
-    );
+	const history = useHistory();
+	const { openScreen } = useIntermediate();
+	const [status, setStatus] = useState(ClientStatus.INIT);
+	const [client, setClient] = useState<Client>(
+		undefined as unknown as Client,
+	);
+
+	useEffect(() => {
+		(async () => {
+			let db;
+			try {
+				// Match sw.ts#L23
+				db = await openDB("state", 3, {
+					upgrade(db) {
+						for (let store of [
+							"channels",
+							"servers",
+							"users",
+							"members",
+						]) {
+							db.createObjectStore(store, {
+								keyPath: "_id",
+							});
+						}
+					},
+				});
+			} catch (err) {
+				console.error(
+					"Failed to open IndexedDB store, continuing without.",
+				);
+			}
+
+			const client = new Client({
+				autoReconnect: false,
+				apiURL: import.meta.env.VITE_API_URL,
+				debug: import.meta.env.DEV,
+				db,
+			});
+
+			setClient(client);
+			SingletonMessageRenderer.subscribe(client);
+			setStatus(ClientStatus.LOADING);
+		})();
+	}, []);
+
+	if (status === ClientStatus.INIT) return null;
+
+	const operations: ClientOperations = useMemo(() => {
+		return {
+			login: async (data) => {
+				setReconnectDisallowed(true);
+
+				try {
+					const onboarding = await client.login(data);
+					setReconnectDisallowed(false);
+					const login = () =>
+						dispatch({
+							type: "LOGIN",
+							session: client.session!, // This [null assertion] is ok, we should have a session by now. - insert's words
+						});
+
+					if (onboarding) {
+						openScreen({
+							id: "onboarding",
+							callback: (username: string) =>
+								onboarding(username, true).then(login),
+						});
+					} else {
+						login();
+					}
+				} catch (err) {
+					setReconnectDisallowed(false);
+					throw err;
+				}
+			},
+			logout: async (shouldRequest) => {
+				dispatch({ type: "LOGOUT" });
+
+				client.reset();
+				dispatch({ type: "RESET" });
+
+				openScreen({ id: "none" });
+				setStatus(ClientStatus.READY);
+
+				client.websocket.disconnect();
+
+				if (shouldRequest) {
+					try {
+						await client.logout();
+					} catch (err) {
+						console.error(err);
+					}
+				}
+			},
+			loggedIn: () => typeof auth.active !== "undefined",
+			ready: () =>
+				operations.loggedIn() && typeof client.user !== "undefined",
+			openDM: async (user_id: string) => {
+				let channel = await client.users.openDM(user_id);
+				history.push(`/channel/${channel!._id}`);
+				return channel!._id;
+			},
+		};
+	}, [client, auth.active]);
+
+	useEffect(
+		() => registerEvents({ operations }, setStatus, client),
+		[client],
+	);
+
+	useEffect(() => {
+		(async () => {
+			if (client.db) {
+				await client.restore();
+			}
+
+			if (auth.active) {
+				dispatch({ type: "QUEUE_FAIL_ALL" });
+
+				const active = auth.accounts[auth.active];
+				client.user = client.users.get(active.session.user_id);
+				if (!navigator.onLine) {
+					return setStatus(ClientStatus.OFFLINE);
+				}
+
+				if (operations.ready()) setStatus(ClientStatus.CONNECTING);
+
+				if (navigator.onLine) {
+					await client
+						.fetchConfiguration()
+						.catch(() =>
+							console.error("Failed to connect to API server."),
+						);
+				}
+
+				try {
+					await client.fetchConfiguration();
+					const callback = await client.useExistingSession(
+						active.session,
+					);
+
+					if (callback) {
+						openScreen({ id: "onboarding", callback });
+					}
+				} catch (err) {
+					setStatus(ClientStatus.DISCONNECTED);
+					const error = takeError(err);
+					if (error === "Forbidden" || error === "Unauthorized") {
+						operations.logout(true);
+						openScreen({ id: "signed_out" });
+					} else {
+						openScreen({ id: "error", error });
+					}
+				}
+			} else {
+				try {
+					await client.fetchConfiguration();
+				} catch (err) {
+					console.error("Failed to connect to API server.");
+				}
+
+				setStatus(ClientStatus.READY);
+			}
+		})();
+	}, []);
+
+	if (status === ClientStatus.LOADING) {
+		return <Preloader type="spinner" />;
+	}
+
+	return (
+		<AppContext.Provider value={client}>
+			<StatusContext.Provider value={status}>
+				<OperationsContext.Provider value={operations}>
+					{children}
+				</OperationsContext.Provider>
+			</StatusContext.Provider>
+		</AppContext.Provider>
+	);
 }
 
-export default connectState<{ children: Children }>(
-    Context,
-    state => {
-        return {
-            auth: state.auth,
-            sync: state.sync
-        };
-    }
-);
+export default connectState<{ children: Children }>(Context, (state) => {
+	return {
+		auth: state.auth,
+		sync: state.sync,
+	};
+});
diff --git a/src/context/revoltjs/StateMonitor.tsx b/src/context/revoltjs/StateMonitor.tsx
index cc0d7616196af52da5a2233eb48ddb8400c96164..f15d353aa3a548e379234428540e3f18ef5cac1a 100644
--- a/src/context/revoltjs/StateMonitor.tsx
+++ b/src/context/revoltjs/StateMonitor.tsx
@@ -1,77 +1,76 @@
 /**
  * This file monitors the message cache to delete any queued messages that have already sent.
  */
-
 import { Message } from "revolt.js";
-import { AppContext } from "./RevoltClient";
-import { Typing } from "../../redux/reducers/typing";
+
 import { useContext, useEffect } from "preact/hooks";
+
+import { dispatch } from "../../redux";
 import { connectState } from "../../redux/connector";
 import { QueuedMessage } from "../../redux/reducers/queue";
-import { dispatch } from "../../redux";
+import { Typing } from "../../redux/reducers/typing";
+
+import { AppContext } from "./RevoltClient";
 
 type Props = {
-    messages: QueuedMessage[];
-    typing: Typing
+	messages: QueuedMessage[];
+	typing: Typing;
 };
 
 function StateMonitor(props: Props) {
-    const client = useContext(AppContext);
+	const client = useContext(AppContext);
 
-    useEffect(() => {
-        dispatch({
-            type: 'QUEUE_DROP_ALL'
-        });
-    }, [ ]);
+	useEffect(() => {
+		dispatch({
+			type: "QUEUE_DROP_ALL",
+		});
+	}, []);
 
-    useEffect(() => {
-        function add(msg: Message) {
-            if (!msg.nonce) return;
-            if (!props.messages.find(x => x.id === msg.nonce)) return;
+	useEffect(() => {
+		function add(msg: Message) {
+			if (!msg.nonce) return;
+			if (!props.messages.find((x) => x.id === msg.nonce)) return;
 
-            dispatch({
-                type: 'QUEUE_REMOVE',
-                nonce: msg.nonce
-            });
-        }
+			dispatch({
+				type: "QUEUE_REMOVE",
+				nonce: msg.nonce,
+			});
+		}
 
-        client.addListener('message', add);
-        return () => client.removeListener('message', add);
-    }, [ props.messages ]);
+		client.addListener("message", add);
+		return () => client.removeListener("message", add);
+	}, [props.messages]);
 
-    useEffect(() => {
-        function removeOld() {
-            if (!props.typing) return;
-            for (let channel of Object.keys(props.typing)) {
-                let users = props.typing[channel];
+	useEffect(() => {
+		function removeOld() {
+			if (!props.typing) return;
+			for (let channel of Object.keys(props.typing)) {
+				let users = props.typing[channel];
 
-                for (let user of users) {
-                    if (+ new Date() > user.started + 5000) {
-                        dispatch({
-                            type: 'TYPING_STOP',
-                            channel,
-                            user: user.id
-                        });
-                    }
-                }
-            }
-        }
+				for (let user of users) {
+					if (+new Date() > user.started + 5000) {
+						dispatch({
+							type: "TYPING_STOP",
+							channel,
+							user: user.id,
+						});
+					}
+				}
+			}
+		}
 
-        removeOld();
+		removeOld();
 
-        let interval = setInterval(removeOld, 1000);
-        return () => clearInterval(interval);
-    }, [ props.typing ]);
+		let interval = setInterval(removeOld, 1000);
+		return () => clearInterval(interval);
+	}, [props.typing]);
 
-    return null;
+	return null;
 }
 
-export default connectState(
-    StateMonitor,
-    state => {
-        return {
-            messages: [...state.queue],
-            typing: state.typing
-        };
-    }
-);
+export default connectState(StateMonitor, (state) => {
+	return {
+		messages: [...state.queue],
+		typing: state.typing,
+	};
+});
diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx
index f09ffb0d1ca7477e4c715085267f541b85f680e5..ee24d525c1179634dd21dc146ad4be7b864419fc 100644
--- a/src/context/revoltjs/SyncManager.tsx
+++ b/src/context/revoltjs/SyncManager.tsx
@@ -1,126 +1,146 @@
 /**
  * This file monitors changes to settings and syncs them to the server.
  */
-
 import isEqual from "lodash.isequal";
-import { Language } from "../Locale";
 import { Sync } from "revolt.js/dist/api/objects";
+import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
+
 import { useContext, useEffect } from "preact/hooks";
+
+import { dispatch } from "../../redux";
 import { connectState } from "../../redux/connector";
-import { Settings } from "../../redux/reducers/settings";
 import { Notifications } from "../../redux/reducers/notifications";
+import { Settings } from "../../redux/reducers/settings";
+import {
+	DEFAULT_ENABLED_SYNC,
+	SyncData,
+	SyncKeys,
+	SyncOptions,
+} from "../../redux/reducers/sync";
+
+import { Language } from "../Locale";
 import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
-import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
-import { DEFAULT_ENABLED_SYNC, SyncData, SyncKeys, SyncOptions } from "../../redux/reducers/sync";
-import { dispatch } from "../../redux";
 
 type Props = {
-    settings: Settings,
-    locale: Language,
-    sync: SyncOptions,
-    notifications: Notifications
+	settings: Settings;
+	locale: Language;
+	sync: SyncOptions;
+	notifications: Notifications;
 };
 
-var lastValues: { [key in SyncKeys]?: any } = { };
-
-export function mapSync(packet: Sync.UserSettings, revision?: Record<string, number>) {
-    let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
-    for (let key of Object.keys(packet)) {
-        let [ timestamp, obj ] = packet[key];
-        if (timestamp < (revision ?? {})[key] ?? 0) {
-            continue;
-        }
-
-        let object;
-        if (obj[0] === '{') {
-            object = JSON.parse(obj)
-        } else {
-            object = obj;
-        }
-
-        lastValues[key as SyncKeys] = object;
-        update[key as SyncKeys] = [ timestamp, object ];
-    }
-
-    return update;
+var lastValues: { [key in SyncKeys]?: any } = {};
+
+export function mapSync(
+	packet: Sync.UserSettings,
+	revision?: Record<string, number>,
+) {
+	let update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
+	for (let key of Object.keys(packet)) {
+		let [timestamp, obj] = packet[key];
+		if (timestamp < (revision ?? {})[key] ?? 0) {
+			continue;
+		}
+
+		let object;
+		if (obj[0] === "{") {
+			object = JSON.parse(obj);
+		} else {
+			object = obj;
+		}
+
+		lastValues[key as SyncKeys] = object;
+		update[key as SyncKeys] = [timestamp, object];
+	}
+
+	return update;
 }
 
 function SyncManager(props: Props) {
-    const client = useContext(AppContext);
-    const status = useContext(StatusContext);
-
-    useEffect(() => {
-        if (status === ClientStatus.ONLINE) {
-            client
-                .syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
-                .then(data => {
-                    dispatch({
-                        type: 'SYNC_UPDATE',
-                        update: mapSync(data)
-                    });
-                });
-
-            client
-                .syncFetchUnreads()
-                .then(unreads => dispatch({ type: 'UNREADS_SET', unreads }));
-        }
-    }, [ status ]);
-
-    function syncChange(key: SyncKeys, data: any) {
-        let timestamp = + new Date();
-        dispatch({
-            type: 'SYNC_SET_REVISION',
-            key,
-            timestamp
-        });
-
-        client.syncSetSettings({
-            [key]: data
-        }, timestamp);
-    }
-
-    let disabled = props.sync.disabled ?? [];
-    for (let [key, object] of [ ['appearance', props.settings.appearance], ['theme', props.settings.theme], ['locale', props.locale], ['notifications', props.notifications] ] as [SyncKeys, any][]) {
-        useEffect(() => {
-            if (disabled.indexOf(key) === -1) {
-                if (typeof lastValues[key] !== 'undefined') {
-                    if (!isEqual(lastValues[key], object)) {
-                        syncChange(key, object);
-                    }
-                }
-            }
-
-            lastValues[key] = object;
-        }, [ disabled, object ]);
-    }
-
-    useEffect(() => {
-        function onPacket(packet: ClientboundNotification) {
-            if (packet.type === 'UserSettingsUpdate') {
-                let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = mapSync(packet.update, props.sync.revision);
-
-                dispatch({
-                    type: 'SYNC_UPDATE',
-                    update
-                });
-            }
-        }
-
-        client.addListener('packet', onPacket);
-        return () => client.removeListener('packet', onPacket);
-    }, [ disabled, props.sync ]);
-
-    return null;
+	const client = useContext(AppContext);
+	const status = useContext(StatusContext);
+
+	useEffect(() => {
+		if (status === ClientStatus.ONLINE) {
+			client
+				.syncFetchSettings(
+					DEFAULT_ENABLED_SYNC.filter(
+						(x) => !props.sync?.disabled?.includes(x),
+					),
+				)
+				.then((data) => {
+					dispatch({
+						type: "SYNC_UPDATE",
+						update: mapSync(data),
+					});
+				});
+
+			client
+				.syncFetchUnreads()
+				.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
+		}
+	}, [status]);
+
+	function syncChange(key: SyncKeys, data: any) {
+		let timestamp = +new Date();
+		dispatch({
+			type: "SYNC_SET_REVISION",
+			key,
+			timestamp,
+		});
+
+		client.syncSetSettings(
+			{
+				[key]: data,
+			},
+			timestamp,
+		);
+	}
+
+	let disabled = props.sync.disabled ?? [];
+	for (let [key, object] of [
+		["appearance", props.settings.appearance],
+		["theme", props.settings.theme],
+		["locale", props.locale],
+		["notifications", props.notifications],
+	] as [SyncKeys, any][]) {
+		useEffect(() => {
+			if (disabled.indexOf(key) === -1) {
+				if (typeof lastValues[key] !== "undefined") {
+					if (!isEqual(lastValues[key], object)) {
+						syncChange(key, object);
+					}
+				}
+			}
+
+			lastValues[key] = object;
+		}, [disabled, object]);
+	}
+
+	useEffect(() => {
+		function onPacket(packet: ClientboundNotification) {
+			if (packet.type === "UserSettingsUpdate") {
+				let update: { [key in SyncKeys]?: [number, SyncData[key]] } =
+					mapSync(packet.update, props.sync.revision);
+
+				dispatch({
+					type: "SYNC_UPDATE",
+					update,
+				});
+			}
+		}
+
+		client.addListener("packet", onPacket);
+		return () => client.removeListener("packet", onPacket);
+	}, [disabled, props.sync]);
+
+	return null;
 }
 
-export default connectState(
-    SyncManager,
-    state => {
-        return {
-            settings: state.settings,
-            locale: state.locale,
-            sync: state.sync,
-            notifications: state.notifications
-        };
-    }
-);
+export default connectState(SyncManager, (state) => {
+	return {
+		settings: state.settings,
+		locale: state.locale,
+		sync: state.sync,
+		notifications: state.notifications,
+	};
+});
diff --git a/src/context/revoltjs/events.ts b/src/context/revoltjs/events.ts
index 319894689955981271fff384005383b3855eee60..b603f0876366eec12b132f34c96011319262ac38 100644
--- a/src/context/revoltjs/events.ts
+++ b/src/context/revoltjs/events.ts
@@ -1,148 +1,155 @@
-import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
 import { Client, Message } from "revolt.js/dist";
-import {
-    ClientOperations,
-    ClientStatus
-} from "./RevoltClient";
+import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
+
 import { StateUpdater } from "preact/hooks";
+
 import { dispatch } from "../../redux";
 
+import { ClientOperations, ClientStatus } from "./RevoltClient";
+
 export var preventReconnect = false;
 let preventUntil = 0;
 
 export function setReconnectDisallowed(allowed: boolean) {
-    preventReconnect = allowed;
+	preventReconnect = allowed;
 }
 
-export function registerEvents({
-    operations
-}: { operations: ClientOperations }, setStatus: StateUpdater<ClientStatus>, client: Client) {
-    function attemptReconnect() {
-        if (preventReconnect) return;
-        function reconnect() {
-            preventUntil = +new Date() + 2000;
-            client.websocket.connect().catch(err => console.error(err));
-        }
-
-        if (+new Date() > preventUntil) {
-            setTimeout(reconnect, 2000);
-        } else {
-            reconnect();
-        }
-    }
-
-    let listeners: Record<string, (...args: any[]) => void> = {
-        connecting: () =>
-            operations.ready() && setStatus(ClientStatus.CONNECTING),
-
-        dropped: () => {
-            if (operations.ready()) {
-                setStatus(ClientStatus.DISCONNECTED);
-                attemptReconnect();
-            }
-        },
-
-        packet: (packet: ClientboundNotification) => {
-            switch (packet.type) {
-                case "ChannelStartTyping": {
-                    if (packet.user === client.user?._id) return;
-                    dispatch({
-                        type: "TYPING_START",
-                        channel: packet.id,
-                        user: packet.user
-                    });
-                    break;
-                }
-                case "ChannelStopTyping": {
-                    if (packet.user === client.user?._id) return;
-                    dispatch({
-                        type: "TYPING_STOP",
-                        channel: packet.id,
-                        user: packet.user
-                    });
-                    break;
-                }
-                case "ChannelAck": {
-                    dispatch({
-                        type: "UNREADS_MARK_READ",
-                        channel: packet.id,
-                        message: packet.message_id
-                    });
-                    break;
-                }
-            }
-        },
-
-        message: (message: Message) => {
-            if (message.mentions?.includes(client.user!._id)) {
-                dispatch({
-                    type: "UNREADS_MENTION",
-                    channel: message.channel,
-                    message: message._id
-                });
-            }
-        },
-
-        ready: () => setStatus(ClientStatus.ONLINE)
-    };
-
-    if (import.meta.env.DEV) {
-        listeners = new Proxy(listeners, {
-            get: (target, listener, receiver) => (...args: unknown[]) => {
-                console.debug(`Calling ${listener.toString()} with`, args);
-                Reflect.get(target, listener)(...args)
-            }
-        })
-    }
-
-    // TODO: clean this a bit and properly handle types
-    for (const listener in listeners) {
-        client.addListener(listener, listeners[listener]);
-    }
-
-    function logMutation(target: string, key: string) {
-        console.log('(o) Object mutated', target, '\nChanged:', key);
-    }
-
-    if (import.meta.env.DEV) {
-        client.users.addListener('mutation', logMutation);
-        client.servers.addListener('mutation', logMutation);
-        client.channels.addListener('mutation', logMutation);
-        client.servers.members.addListener('mutation', logMutation);
-    }
-
-    const online = () => {
-        if (operations.ready()) {
-            setStatus(ClientStatus.RECONNECTING);
-            setReconnectDisallowed(false);
-            attemptReconnect();
-        }
-    };
-    
-    const offline = () => {
-        if (operations.ready()) {
-            setReconnectDisallowed(true);
-            client.websocket.disconnect();
-            setStatus(ClientStatus.OFFLINE);
-        }
-    };
-
-    window.addEventListener("online", online);
-    window.addEventListener("offline", offline);
-
-    return () => {
-        for (const listener in listeners) {
-            client.removeListener(listener, listeners[listener as keyof typeof listeners]);
-        }
-
-        if (import.meta.env.DEV) {
-            client.users.removeListener('mutation', logMutation);
-            client.servers.removeListener('mutation', logMutation);
-            client.channels.removeListener('mutation', logMutation);
-            client.servers.members.removeListener('mutation', logMutation);
-        }
-
-        window.removeEventListener("online", online);
-        window.removeEventListener("offline", offline);
-    };
+export function registerEvents(
+	{ operations }: { operations: ClientOperations },
+	setStatus: StateUpdater<ClientStatus>,
+	client: Client,
+) {
+	function attemptReconnect() {
+		if (preventReconnect) return;
+		function reconnect() {
+			preventUntil = +new Date() + 2000;
+			client.websocket.connect().catch((err) => console.error(err));
+		}
+
+		if (+new Date() > preventUntil) {
+			setTimeout(reconnect, 2000);
+		} else {
+			reconnect();
+		}
+	}
+
+	let listeners: Record<string, (...args: any[]) => void> = {
+		connecting: () =>
+			operations.ready() && setStatus(ClientStatus.CONNECTING),
+
+		dropped: () => {
+			if (operations.ready()) {
+				setStatus(ClientStatus.DISCONNECTED);
+				attemptReconnect();
+			}
+		},
+
+		packet: (packet: ClientboundNotification) => {
+			switch (packet.type) {
+				case "ChannelStartTyping": {
+					if (packet.user === client.user?._id) return;
+					dispatch({
+						type: "TYPING_START",
+						channel: packet.id,
+						user: packet.user,
+					});
+					break;
+				}
+				case "ChannelStopTyping": {
+					if (packet.user === client.user?._id) return;
+					dispatch({
+						type: "TYPING_STOP",
+						channel: packet.id,
+						user: packet.user,
+					});
+					break;
+				}
+				case "ChannelAck": {
+					dispatch({
+						type: "UNREADS_MARK_READ",
+						channel: packet.id,
+						message: packet.message_id,
+					});
+					break;
+				}
+			}
+		},
+
+		message: (message: Message) => {
+			if (message.mentions?.includes(client.user!._id)) {
+				dispatch({
+					type: "UNREADS_MENTION",
+					channel: message.channel,
+					message: message._id,
+				});
+			}
+		},
+
+		ready: () => setStatus(ClientStatus.ONLINE),
+	};
+
+	if (import.meta.env.DEV) {
+		listeners = new Proxy(listeners, {
+			get:
+				(target, listener, receiver) =>
+				(...args: unknown[]) => {
+					console.debug(`Calling ${listener.toString()} with`, args);
+					Reflect.get(target, listener)(...args);
+				},
+		});
+	}
+
+	// TODO: clean this a bit and properly handle types
+	for (const listener in listeners) {
+		client.addListener(listener, listeners[listener]);
+	}
+
+	function logMutation(target: string, key: string) {
+		console.log("(o) Object mutated", target, "\nChanged:", key);
+	}
+
+	if (import.meta.env.DEV) {
+		client.users.addListener("mutation", logMutation);
+		client.servers.addListener("mutation", logMutation);
+		client.channels.addListener("mutation", logMutation);
+		client.servers.members.addListener("mutation", logMutation);
+	}
+
+	const online = () => {
+		if (operations.ready()) {
+			setStatus(ClientStatus.RECONNECTING);
+			setReconnectDisallowed(false);
+			attemptReconnect();
+		}
+	};
+
+	const offline = () => {
+		if (operations.ready()) {
+			setReconnectDisallowed(true);
+			client.websocket.disconnect();
+			setStatus(ClientStatus.OFFLINE);
+		}
+	};
+
+	window.addEventListener("online", online);
+	window.addEventListener("offline", offline);
+
+	return () => {
+		for (const listener in listeners) {
+			client.removeListener(
+				listener,
+				listeners[listener as keyof typeof listeners],
+			);
+		}
+
+		if (import.meta.env.DEV) {
+			client.users.removeListener("mutation", logMutation);
+			client.servers.removeListener("mutation", logMutation);
+			client.channels.removeListener("mutation", logMutation);
+			client.servers.members.removeListener("mutation", logMutation);
+		}
+
+		window.removeEventListener("online", online);
+		window.removeEventListener("offline", offline);
+	};
 }
diff --git a/src/context/revoltjs/hooks.ts b/src/context/revoltjs/hooks.ts
index fac889862a8075df4e1e85707062b9e09d06a172..ffab5c9324a9f30f96a612980af8c8aee945fef0 100644
--- a/src/context/revoltjs/hooks.ts
+++ b/src/context/revoltjs/hooks.ts
@@ -1,177 +1,232 @@
-import { useCallback, useContext, useEffect, useState } from "preact/hooks";
+import { Client, PermissionCalculator } from "revolt.js";
 import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
-import { Client, PermissionCalculator } from 'revolt.js';
-import { AppContext } from "./RevoltClient";
 import Collection from "revolt.js/dist/maps/Collection";
 
+import { useCallback, useContext, useEffect, useState } from "preact/hooks";
+
+import { AppContext } from "./RevoltClient";
+
 export interface HookContext {
-    client: Client,
-    forceUpdate: () => void
+	client: Client;
+	forceUpdate: () => void;
 }
 
 export function useForceUpdate(context?: HookContext): HookContext {
-    const client = useContext(AppContext);
-    if (context) return context;
-
-    const H = useState(0);
-    var updateState: (_: number) => void;
-    if (Array.isArray(H)) {
-        let [, u] = H;
-        updateState = u;
-    } else {
-        console.warn('Failed to construct using useState.');
-        updateState = ()=>{};
-    }
-
-    return { client, forceUpdate: () => updateState(Math.random()) };
+	const client = useContext(AppContext);
+	if (context) return context;
+
+	const H = useState(0);
+	var updateState: (_: number) => void;
+	if (Array.isArray(H)) {
+		let [, u] = H;
+		updateState = u;
+	} else {
+		console.warn("Failed to construct using useState.");
+		updateState = () => {};
+	}
+
+	return { client, forceUpdate: () => updateState(Math.random()) };
 }
 
 // TODO: utils.d.ts maybe?
-type PickProperties<T, U> = Pick<T, {
-    [K in keyof T]: T[K] extends U ? K : never
-}[keyof T]>
+type PickProperties<T, U> = Pick<
+	T,
+	{
+		[K in keyof T]: T[K] extends U ? K : never;
+	}[keyof T]
+>;
 
 // The keys in Client that are an object
 // for some reason undefined keeps appearing despite there being no reason to so it's filtered out
-type ClientCollectionKey = Exclude<keyof PickProperties<Client, Collection<any>>, undefined>;
-
-function useObject(type: ClientCollectionKey, id?: string | string[], context?: HookContext) {
-    const ctx = useForceUpdate(context);
-
-    function update(target: any) {
-        if (typeof id === 'string' ? target === id :
-            Array.isArray(id) ? id.includes(target) : true) {
-            ctx.forceUpdate();
-        }
-    }
-
-    const map = ctx.client[type];
-    useEffect(() => {
-        map.addListener("update", update);
-        return () => map.removeListener("update", update);
-    }, [id]);
-
-    return typeof id === 'string' ? map.get(id)
-        : Array.isArray(id) ? id.map(x => map.get(x))
-        : map.toArray();
+type ClientCollectionKey = Exclude<
+	keyof PickProperties<Client, Collection<any>>,
+	undefined
+>;
+
+function useObject(
+	type: ClientCollectionKey,
+	id?: string | string[],
+	context?: HookContext,
+) {
+	const ctx = useForceUpdate(context);
+
+	function update(target: any) {
+		if (
+			typeof id === "string"
+				? target === id
+				: Array.isArray(id)
+				? id.includes(target)
+				: true
+		) {
+			ctx.forceUpdate();
+		}
+	}
+
+	const map = ctx.client[type];
+	useEffect(() => {
+		map.addListener("update", update);
+		return () => map.removeListener("update", update);
+	}, [id]);
+
+	return typeof id === "string"
+		? map.get(id)
+		: Array.isArray(id)
+		? id.map((x) => map.get(x))
+		: map.toArray();
 }
 
 export function useUser(id?: string, context?: HookContext) {
-    if (typeof id === "undefined") return;
-    return useObject('users', id, context) as Readonly<Users.User> | undefined;
+	if (typeof id === "undefined") return;
+	return useObject("users", id, context) as Readonly<Users.User> | undefined;
 }
 
 export function useSelf(context?: HookContext) {
-    const ctx = useForceUpdate(context);
-    return useUser(ctx.client.user!._id, ctx);
+	const ctx = useForceUpdate(context);
+	return useUser(ctx.client.user!._id, ctx);
 }
 
 export function useUsers(ids?: string[], context?: HookContext) {
-    return useObject('users', ids, context) as (Readonly<Users.User> | undefined)[];
+	return useObject("users", ids, context) as (
+		| Readonly<Users.User>
+		| undefined
+	)[];
 }
 
 export function useChannel(id?: string, context?: HookContext) {
-    if (typeof id === "undefined") return;
-    return useObject('channels', id, context) as Readonly<Channels.Channel> | undefined;
+	if (typeof id === "undefined") return;
+	return useObject("channels", id, context) as
+		| Readonly<Channels.Channel>
+		| undefined;
 }
 
 export function useChannels(ids?: string[], context?: HookContext) {
-    return useObject('channels', ids, context) as (Readonly<Channels.Channel> | undefined)[];
+	return useObject("channels", ids, context) as (
+		| Readonly<Channels.Channel>
+		| undefined
+	)[];
 }
 
 export function useServer(id?: string, context?: HookContext) {
-    if (typeof id === "undefined") return;
-    return useObject('servers', id, context) as Readonly<Servers.Server> | undefined;
+	if (typeof id === "undefined") return;
+	return useObject("servers", id, context) as
+		| Readonly<Servers.Server>
+		| undefined;
 }
 
 export function useServers(ids?: string[], context?: HookContext) {
-    return useObject('servers', ids, context) as (Readonly<Servers.Server> | undefined)[];
+	return useObject("servers", ids, context) as (
+		| Readonly<Servers.Server>
+		| undefined
+	)[];
 }
 
 export function useDMs(context?: HookContext) {
-    const ctx = useForceUpdate(context);
-
-    function mutation(target: string) {
-        let channel = ctx.client.channels.get(target);
-        if (channel) {
-            if (channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group') {
-                ctx.forceUpdate();
-            }
-        }
-    }
-
-    const map = ctx.client.channels;
-    useEffect(() => {
-        map.addListener("update", mutation);
-        return () => map.removeListener("update", mutation);
-    }, []);
-
-    return map
-        .toArray()
-        .filter(x => x.channel_type === 'DirectMessage' || x.channel_type === 'Group' || x.channel_type === 'SavedMessages') as (Channels.GroupChannel | Channels.DirectMessageChannel | Channels.SavedMessagesChannel)[];
+	const ctx = useForceUpdate(context);
+
+	function mutation(target: string) {
+		let channel = ctx.client.channels.get(target);
+		if (channel) {
+			if (
+				channel.channel_type === "DirectMessage" ||
+				channel.channel_type === "Group"
+			) {
+				ctx.forceUpdate();
+			}
+		}
+	}
+
+	const map = ctx.client.channels;
+	useEffect(() => {
+		map.addListener("update", mutation);
+		return () => map.removeListener("update", mutation);
+	}, []);
+
+	return map
+		.toArray()
+		.filter(
+			(x) =>
+				x.channel_type === "DirectMessage" ||
+				x.channel_type === "Group" ||
+				x.channel_type === "SavedMessages",
+		) as (
+		| Channels.GroupChannel
+		| Channels.DirectMessageChannel
+		| Channels.SavedMessagesChannel
+	)[];
 }
 
 export function useUserPermission(id: string, context?: HookContext) {
-    const ctx = useForceUpdate(context);
-
-    const mutation = (target: string) => (target === id) && ctx.forceUpdate();
-    useEffect(() => {
-        ctx.client.users.addListener("update", mutation);
-        return () => ctx.client.users.removeListener("update", mutation);
-    }, [id]);
-    
-    let calculator = new PermissionCalculator(ctx.client);
-    return calculator.forUser(id);
+	const ctx = useForceUpdate(context);
+
+	const mutation = (target: string) => target === id && ctx.forceUpdate();
+	useEffect(() => {
+		ctx.client.users.addListener("update", mutation);
+		return () => ctx.client.users.removeListener("update", mutation);
+	}, [id]);
+
+	let calculator = new PermissionCalculator(ctx.client);
+	return calculator.forUser(id);
 }
 
 export function useChannelPermission(id: string, context?: HookContext) {
-    const ctx = useForceUpdate(context);
-
-    const channel = ctx.client.channels.get(id);
-    const server = (channel && (channel.channel_type === 'TextChannel' || channel.channel_type === 'VoiceChannel')) ? channel.server : undefined;
-
-    const mutation = (target: string) => (target === id) && ctx.forceUpdate();
-    const mutationServer = (target: string) => (target === server) && ctx.forceUpdate();
-    const mutationMember = (target: string) => (target.substr(26) === ctx.client.user!._id) && ctx.forceUpdate();
-
-    useEffect(() => {
-        ctx.client.channels.addListener("update", mutation);
-
-        if (server) {
-            ctx.client.servers.addListener("update", mutationServer);
-            ctx.client.servers.members.addListener("update", mutationMember);
-        }
-
-        return () => {
-            ctx.client.channels.removeListener("update", mutation);
-
-            if (server) {
-                ctx.client.servers.removeListener("update", mutationServer);
-                ctx.client.servers.members.removeListener("update", mutationMember);
-            }
-        }
-    }, [id]);
-    
-    let calculator = new PermissionCalculator(ctx.client);
-    return calculator.forChannel(id);
+	const ctx = useForceUpdate(context);
+
+	const channel = ctx.client.channels.get(id);
+	const server =
+		channel &&
+		(channel.channel_type === "TextChannel" ||
+			channel.channel_type === "VoiceChannel")
+			? channel.server
+			: undefined;
+
+	const mutation = (target: string) => target === id && ctx.forceUpdate();
+	const mutationServer = (target: string) =>
+		target === server && ctx.forceUpdate();
+	const mutationMember = (target: string) =>
+		target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
+
+	useEffect(() => {
+		ctx.client.channels.addListener("update", mutation);
+
+		if (server) {
+			ctx.client.servers.addListener("update", mutationServer);
+			ctx.client.servers.members.addListener("update", mutationMember);
+		}
+
+		return () => {
+			ctx.client.channels.removeListener("update", mutation);
+
+			if (server) {
+				ctx.client.servers.removeListener("update", mutationServer);
+				ctx.client.servers.members.removeListener(
+					"update",
+					mutationMember,
+				);
+			}
+		};
+	}, [id]);
+
+	let calculator = new PermissionCalculator(ctx.client);
+	return calculator.forChannel(id);
 }
 
 export function useServerPermission(id: string, context?: HookContext) {
-    const ctx = useForceUpdate(context);
-
-    const mutation = (target: string) => (target === id) && ctx.forceUpdate();
-    const mutationMember = (target: string) => (target.substr(26) === ctx.client.user!._id) && ctx.forceUpdate();
-
-    useEffect(() => {
-        ctx.client.servers.addListener("update", mutation);
-        ctx.client.servers.members.addListener("update", mutationMember);
-
-        return () => {
-            ctx.client.servers.removeListener("update", mutation);
-            ctx.client.servers.members.removeListener("update", mutationMember);
-        }
-    }, [id]);
-    
-    let calculator = new PermissionCalculator(ctx.client);
-    return calculator.forServer(id);
+	const ctx = useForceUpdate(context);
+
+	const mutation = (target: string) => target === id && ctx.forceUpdate();
+	const mutationMember = (target: string) =>
+		target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
+
+	useEffect(() => {
+		ctx.client.servers.addListener("update", mutation);
+		ctx.client.servers.members.addListener("update", mutationMember);
+
+		return () => {
+			ctx.client.servers.removeListener("update", mutation);
+			ctx.client.servers.members.removeListener("update", mutationMember);
+		};
+	}, [id]);
+
+	let calculator = new PermissionCalculator(ctx.client);
+	return calculator.forServer(id);
 }
diff --git a/src/context/revoltjs/util.tsx b/src/context/revoltjs/util.tsx
index a611d3e1eac8d3c80cbc80e8cfbb2e249fb05605..e575c71119220b27ff956e1379cc1f1f77a143d9 100644
--- a/src/context/revoltjs/util.tsx
+++ b/src/context/revoltjs/util.tsx
@@ -1,48 +1,57 @@
-import { Channel, Message, User } from "revolt.js/dist/api/objects";
-import { Children } from "../../types/Preact";
-import { Text } from "preact-i18n";
 import { Client } from "revolt.js";
+import { Channel, Message, User } from "revolt.js/dist/api/objects";
 
-export function takeError(
-    error: any
-): string {
-    const type = error?.response?.data?.type;
-    let id = type;
-    if (!type) {
-        if (error?.response?.status === 403) {
-            return "Unauthorized";
-        } else if (error && (!!error.isAxiosError && !error.response)) {
-            return "NetworkError";
-        }
-
-        console.error(error);
-        return "UnknownError";
-    }
-
-    return id;
-}
-
-export function getChannelName(client: Client, channel: Channel, prefixType?: boolean): Children {
-    if (channel.channel_type === "SavedMessages")
-        return <Text id="app.navigation.tabs.saved" />;
+import { Text } from "preact-i18n";
 
-    if (channel.channel_type === "DirectMessage") {
-        let uid = client.channels.getRecipient(channel._id);
-        return <>{prefixType && "@"}{client.users.get(uid)?.username}</>;
-    }
+import { Children } from "../../types/Preact";
 
-    if (channel.channel_type === "TextChannel" && prefixType) {
-        return <>#{channel.name}</>;
-    }
+export function takeError(error: any): string {
+	const type = error?.response?.data?.type;
+	let id = type;
+	if (!type) {
+		if (error?.response?.status === 403) {
+			return "Unauthorized";
+		} else if (error && !!error.isAxiosError && !error.response) {
+			return "NetworkError";
+		}
+
+		console.error(error);
+		return "UnknownError";
+	}
+
+	return id;
+}
 
-    return <>{channel.name}</>;
+export function getChannelName(
+	client: Client,
+	channel: Channel,
+	prefixType?: boolean,
+): Children {
+	if (channel.channel_type === "SavedMessages")
+		return <Text id="app.navigation.tabs.saved" />;
+
+	if (channel.channel_type === "DirectMessage") {
+		let uid = client.channels.getRecipient(channel._id);
+		return (
+			<>
+				{prefixType && "@"}
+				{client.users.get(uid)?.username}
+			</>
+		);
+	}
+
+	if (channel.channel_type === "TextChannel" && prefixType) {
+		return <>#{channel.name}</>;
+	}
+
+	return <>{channel.name}</>;
 }
 
 export type MessageObject = Omit<Message, "edited"> & { edited?: string };
 export function mapMessage(message: Partial<Message>) {
-    const { edited, ...msg } = message;
-    return {
-        ...msg,
-        edited: edited?.$date,
-    } as MessageObject;
+	const { edited, ...msg } = message;
+	return {
+		...msg,
+		edited: edited?.$date,
+	} as MessageObject;
 }
diff --git a/src/env.d.ts b/src/env.d.ts
index 2f894b5f8f587a23f2535639178142b8501fb040..3b2063f055be1ba7b31517802335b778dc87a590 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,5 +1,4 @@
 interface ImportMetaEnv {
-    VITE_API_URL: string;
-    VITE_THEMES_URL: string;
+	VITE_API_URL: string;
+	VITE_THEMES_URL: string;
 }
-  
\ No newline at end of file
diff --git a/src/lib/ConditionalLink.tsx b/src/lib/ConditionalLink.tsx
index b739219dcd380a3e7a47e1862841bcef5b7e6d9e..53f89551ff8a48b476e868a8e07c6386d6c5bbce 100644
--- a/src/lib/ConditionalLink.tsx
+++ b/src/lib/ConditionalLink.tsx
@@ -1,15 +1,16 @@
 import { Link, LinkProps } from "react-router-dom";
 
-type Props = LinkProps & JSX.HTMLAttributes<HTMLAnchorElement> & {
-    active: boolean
-};
+type Props = LinkProps &
+	JSX.HTMLAttributes<HTMLAnchorElement> & {
+		active: boolean;
+	};
 
 export default function ConditionalLink(props: Props) {
-    const { active, ...linkProps } = props;
+	const { active, ...linkProps } = props;
 
-    if (active) {
-        return <a>{ props.children }</a>;
-    } else {
-        return <Link {...linkProps} />;
-    }
+	if (active) {
+		return <a>{props.children}</a>;
+	} else {
+		return <Link {...linkProps} />;
+	}
 }
diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx
index 89f1a6bb87f906c6cde55a35b4ab5b7590a13251..8f73e27141a834a37ac028eef80047e6b2598547 100644
--- a/src/lib/ContextMenus.tsx
+++ b/src/lib/ContextMenus.tsx
@@ -1,764 +1,1018 @@
-import { Text } from "preact-i18n";
-import { useContext } from "preact/hooks";
+import {
+	At,
+	Bell,
+	BellOff,
+	Check,
+	CheckSquare,
+	ChevronRight,
+	Block,
+	Square,
+	LeftArrowAlt,
+	Trash,
+} from "@styled-icons/boxicons-regular";
+import { Cog } from "@styled-icons/boxicons-solid";
 import { useHistory } from "react-router-dom";
-import { Attachment, Channels, Message, Servers, Users } from "revolt.js/dist/api/objects";
 import {
-    ContextMenu,
-    ContextMenuWithData,
-    MenuItem,
-    openContextMenu
+	Attachment,
+	Channels,
+	Message,
+	Servers,
+	Users,
+} from "revolt.js/dist/api/objects";
+import {
+	ChannelPermission,
+	ServerPermission,
+	UserPermission,
+} from "revolt.js/dist/api/permissions";
+
+import {
+	ContextMenu,
+	ContextMenuWithData,
+	MenuItem,
+	openContextMenu,
 } from "preact-context-menu";
-import { ChannelPermission, ServerPermission, UserPermission } from "revolt.js/dist/api/permissions";
+import { Text } from "preact-i18n";
+import { useContext } from "preact/hooks";
+
+import { dispatch } from "../redux";
+import { connectState } from "../redux/connector";
+import {
+	getNotificationState,
+	Notifications,
+	NotificationState,
+} from "../redux/reducers/notifications";
 import { QueuedMessage } from "../redux/reducers/queue";
+
 import { useIntermediate } from "../context/intermediate/Intermediate";
-import { AppContext, ClientStatus, StatusContext } from "../context/revoltjs/RevoltClient";
+import {
+	AppContext,
+	ClientStatus,
+	StatusContext,
+} from "../context/revoltjs/RevoltClient";
+import {
+	useChannel,
+	useChannelPermission,
+	useForceUpdate,
+	useServer,
+	useServerPermission,
+	useUser,
+	useUserPermission,
+} from "../context/revoltjs/hooks";
 import { takeError } from "../context/revoltjs/util";
-import { useChannel, useChannelPermission, useForceUpdate, useServer, useServerPermission, useUser, useUserPermission } from "../context/revoltjs/hooks";
-import { Children } from "../types/Preact";
-import LineDivider from "../components/ui/LineDivider";
-import { connectState } from "../redux/connector";
-import { internalEmit } from "./eventEmitter";
-import { At, Bell, BellOff, Check, CheckSquare, ChevronRight, Block, Square, LeftArrowAlt, Trash } from "@styled-icons/boxicons-regular";
-import { Cog } from "@styled-icons/boxicons-solid";
-import { getNotificationState, Notifications, NotificationState } from "../redux/reducers/notifications";
+
 import UserStatus from "../components/common/user/UserStatus";
 import IconButton from "../components/ui/IconButton";
-import { dispatch } from "../redux";
+import LineDivider from "../components/ui/LineDivider";
+
+import { Children } from "../types/Preact";
+import { internalEmit } from "./eventEmitter";
 
 interface ContextMenuData {
-    user?: string;
-    server?: string;
-    server_list?: string;
-    channel?: string;
-    message?: Message;
-
-    unread?: boolean;
-    queued?: QueuedMessage;
-    contextualChannel?: string;
+	user?: string;
+	server?: string;
+	server_list?: string;
+	channel?: string;
+	message?: Message;
+
+	unread?: boolean;
+	queued?: QueuedMessage;
+	contextualChannel?: string;
 }
 
 type Action =
-    | { action: "copy_id"; id: string }
-    | { action: "copy_selection" }
-    | { action: "copy_text"; content: string }
-    | { action: "mark_as_read"; channel: Channels.Channel }
-    | { action: "retry_message"; message: QueuedMessage }
-    | { action: "cancel_message"; message: QueuedMessage }
-    | { action: "mention"; user: string }
-    | { action: "reply_message"; id: string }
-    | { action: "quote_message"; content: string }
-    | { action: "edit_message"; id: string }
-    | { action: "delete_message"; target: Channels.Message }
-    | { action: "open_file"; attachment: Attachment }
-    | { action: "save_file"; attachment: Attachment }
-    | { action: "copy_file_link"; attachment: Attachment }
-    | { action: "open_link"; link: string }
-    | { action: "copy_link"; link: string }
-    | { action: "remove_member"; channel: string; user: string }
-    | { action: "kick_member"; target: Servers.Server; user: string }
-    | { action: "ban_member"; target: Servers.Server; user: string }
-    | { action: "view_profile"; user: string }
-    | { action: "message_user"; user: string }
-    | { action: "block_user"; user: Users.User }
-    | { action: "unblock_user"; user: Users.User }
-    | { action: "add_friend"; user: Users.User }
-    | { action: "remove_friend"; user: Users.User }
-    | { action: "cancel_friend"; user: Users.User }
-    | { action: "set_presence"; presence: Users.Presence }
-    | { action: "set_status" }
-    | { action: "clear_status" }
-    | { action: "create_channel"; target: Servers.Server }
-    | { action: "create_invite"; target: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel }
-    | { action: "leave_group"; target: Channels.GroupChannel }
-    | { action: "delete_channel"; target: Channels.TextChannel | Channels.VoiceChannel }
-    | { action: "close_dm"; target: Channels.DirectMessageChannel }
-    | { action: "leave_server"; target: Servers.Server }
-    | { action: "delete_server"; target: Servers.Server }
-    | { action: "open_notification_options", channel: Channels.Channel }
-    | { action: "open_settings" }
-    | { action: "open_channel_settings", id: string }
-    | { action: "open_server_settings", id: string }
-    | { action: "open_server_channel_settings", server: string, id: string }
-    | { action: "set_notification_state", key: string, state?: NotificationState };
+	| { action: "copy_id"; id: string }
+	| { action: "copy_selection" }
+	| { action: "copy_text"; content: string }
+	| { action: "mark_as_read"; channel: Channels.Channel }
+	| { action: "retry_message"; message: QueuedMessage }
+	| { action: "cancel_message"; message: QueuedMessage }
+	| { action: "mention"; user: string }
+	| { action: "reply_message"; id: string }
+	| { action: "quote_message"; content: string }
+	| { action: "edit_message"; id: string }
+	| { action: "delete_message"; target: Channels.Message }
+	| { action: "open_file"; attachment: Attachment }
+	| { action: "save_file"; attachment: Attachment }
+	| { action: "copy_file_link"; attachment: Attachment }
+	| { action: "open_link"; link: string }
+	| { action: "copy_link"; link: string }
+	| { action: "remove_member"; channel: string; user: string }
+	| { action: "kick_member"; target: Servers.Server; user: string }
+	| { action: "ban_member"; target: Servers.Server; user: string }
+	| { action: "view_profile"; user: string }
+	| { action: "message_user"; user: string }
+	| { action: "block_user"; user: Users.User }
+	| { action: "unblock_user"; user: Users.User }
+	| { action: "add_friend"; user: Users.User }
+	| { action: "remove_friend"; user: Users.User }
+	| { action: "cancel_friend"; user: Users.User }
+	| { action: "set_presence"; presence: Users.Presence }
+	| { action: "set_status" }
+	| { action: "clear_status" }
+	| { action: "create_channel"; target: Servers.Server }
+	| {
+			action: "create_invite";
+			target:
+				| Channels.GroupChannel
+				| Channels.TextChannel
+				| Channels.VoiceChannel;
+	  }
+	| { action: "leave_group"; target: Channels.GroupChannel }
+	| {
+			action: "delete_channel";
+			target: Channels.TextChannel | Channels.VoiceChannel;
+	  }
+	| { action: "close_dm"; target: Channels.DirectMessageChannel }
+	| { action: "leave_server"; target: Servers.Server }
+	| { action: "delete_server"; target: Servers.Server }
+	| { action: "open_notification_options"; channel: Channels.Channel }
+	| { action: "open_settings" }
+	| { action: "open_channel_settings"; id: string }
+	| { action: "open_server_settings"; id: string }
+	| { action: "open_server_channel_settings"; server: string; id: string }
+	| {
+			action: "set_notification_state";
+			key: string;
+			state?: NotificationState;
+	  };
 
 type Props = {
-    notifications: Notifications
+	notifications: Notifications;
 };
 
 function ContextMenus(props: Props) {
-    const { openScreen, writeClipboard } = useIntermediate();
-    const client = useContext(AppContext);
-    const userId = client.user!._id;
-    const status = useContext(StatusContext);
-    const isOnline = status === ClientStatus.ONLINE;
-    const history = useHistory();
-
-    function contextClick(data?: Action) {
-        if (typeof data === "undefined") return;
-
-        (async () => {
-            switch (data.action) {
-                case "copy_id":
-                    writeClipboard(data.id);
-                    break;
-                case "copy_selection":
-                    writeClipboard(document.getSelection()?.toString() ?? '');
-                    break;
-                case "mark_as_read":
-                    {
-                        if (data.channel.channel_type === 'SavedMessages' ||
-                            data.channel.channel_type === 'VoiceChannel') return;
-
-                        let message = data.channel.channel_type === 'TextChannel' ? data.channel.last_message : data.channel.last_message._id;
-                        dispatch({
-                            type: "UNREADS_MARK_READ",
-                            channel: data.channel._id,
-                            message
-                        });
-
-                        client.req('PUT', `/channels/${data.channel._id}/ack/${message}` as '/channels/id/ack/id');
-                    }
-                    break;
-
-                case "retry_message":
-                    {
-                        const nonce = data.message.id;
-                        const fail = (error: any) =>
-                            dispatch({
-                                type: "QUEUE_FAIL",
-                                nonce,
-                                error
-                            });
-
-                        client.channels
-                            .sendMessage(
-                                data.message.channel,
-                                {
-                                    nonce: data.message.id,
-                                    content: data.message.data.content as string,
-                                    replies: data.message.data.replies
-                                }
-                            )
-                            .catch(fail);
-
-                        dispatch({
-                            type: "QUEUE_START",
-                            nonce
-                        });
-                    }
-                    break;
-
-                case "cancel_message":
-                    {
-                        dispatch({
-                            type: "QUEUE_REMOVE",
-                            nonce: data.message.id
-                        });
-                    }
-                    break;
-
-                case "mention":
-                    {
-                        internalEmit(
-                            "MessageBox",
-                            "append",
-                            `<@${data.user}>`,
-                            "mention"
-                        );
-                    }
-                    break;
-
-                case "copy_text":
-                    writeClipboard(data.content);
-                    break;
-
-                case "reply_message":
-                    {
-                        internalEmit(
-                            "ReplyBar",
-                            "add",
-                            data.id
-                        );
-                    }
-                    break;
-
-                case "quote_message":
-                    {
-                        internalEmit(
-                            "MessageBox",
-                            "append",
-                            data.content,
-                            "quote"
-                        );
-                    }
-                    break;
-
-                case "edit_message":
-                    {
-                        internalEmit("MessageRenderer", "edit_message", data.id);
-                    }
-                    break;
-
-                case "open_file":
-                    {
-                        window
-                            .open(
-                                client.generateFileURL(data.attachment),
-                                "_blank"
-                            )
-                            ?.focus();
-                    }
-                    break;
-
-                case "save_file":
-                    {
-                        window.open(
-                            // ! FIXME: do this from revolt.js
-                            client.generateFileURL(data.attachment)?.replace('attachments', 'attachments/download'),
-                            "_blank"
-                        );
-                    }
-                    break;
-
-                case "copy_file_link":
-                    {
-                        const { filename } = data.attachment;
-                        writeClipboard(
-                            // ! FIXME: do from r.js
-                            client.generateFileURL(data.attachment) + `/${encodeURI(filename)}`,
-                        );
-                    }
-                    break;
-
-                case "open_link":
-                    {
-                        window.open(data.link, "_blank")?.focus();
-                    }
-                    break;
-
-                case "copy_link":
-                    {
-                        writeClipboard(data.link);
-                    }
-                    break;
-
-                case "remove_member":
-                    {
-                        client.channels.removeMember(data.channel, data.user);
-                    }
-                    break;
-
-                case "view_profile":
-                    openScreen({ id: 'profile', user_id: data.user });
-                    break;
-
-                case "message_user":
-                    {
-                        const channel = await client.users.openDM(data.user);
-                        if (channel) {
-                            history.push(`/channel/${channel._id}`);
-                        }
-                    }
-                    break;
-
-                case "add_friend":
-                    {
-                        await client.users.addFriend(data.user.username);
-                    }
-                    break;
-
-                case "block_user":
-                    openScreen({ id: 'special_prompt', type: 'block_user', target: data.user });
-                    break;
-                case "unblock_user":
-                    await client.users.unblockUser(data.user._id);
-                    break;
-                case "remove_friend":
-                    openScreen({ id: 'special_prompt', type: 'unfriend_user', target: data.user });
-                    break;
-                case "cancel_friend":
-                    await client.users.removeFriend(data.user._id);
-                    break;
-
-                case "set_presence":
-                    {
-                        await client.users.editUser({
-                            status: {
-                                ...client.user?.status,
-                                presence: data.presence
-                            }
-                        });
-                    }
-                    break;
-
-                case "set_status": openScreen({ id: "special_input", type: "set_custom_status" }); break;
-
-                case "clear_status":
-                    {
-                        let { text, ...status } = client.user?.status ?? {};
-                        await client.users.editUser({ status });
-                    }
-                    break;
-                
-                case "leave_group":
-                case "close_dm":
-                case "leave_server":
-                case "delete_channel":
-                case "delete_server":
-                case "delete_message":
-                case "create_channel":
-                case "create_invite":
-                    // The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever
-                    openScreen({ id: "special_prompt", type: data.action, target: data.target as any }); 
-                    break;
-
-                case "ban_member":
-                case "kick_member": 
-                    openScreen({ id: "special_prompt", type: data.action, target: data.target, user: data.user });
-                    break;
-
-                case "open_notification_options": {
-                    openContextMenu("NotificationOptions", { channel: data.channel });
-                    break;
-                }
-
-                case "open_settings": history.push('/settings'); break;
-                case "open_channel_settings": history.push(`/channel/${data.id}/settings`); break;
-                case "open_server_channel_settings": history.push(`/server/${data.server}/channel/${data.id}/settings`); break;
-                case "open_server_settings": history.push(`/server/${data.id}/settings`); break;
-
-                case "set_notification_state": {
-                    const { key, state } = data;
-                    if (state) {
-                        dispatch({ type: "NOTIFICATIONS_SET", key, state });
-                    } else {
-                        dispatch({ type: "NOTIFICATIONS_REMOVE", key });
-                    }
-                    break;
-                }
-            }
-        })().catch(err => {
-            openScreen({ id: "error", error: takeError(err) });
-        });
-    }
-
-    return (
-        <>
-            <ContextMenuWithData id="Menu" onClose={contextClick}>
-                {({
-                    user: uid,
-                    channel: cid,
-                    server: sid,
-                    message,
-                    server_list,
-                    queued,
-                    unread,
-                    contextualChannel: cxid
-                }: ContextMenuData) => {
-                    const forceUpdate = useForceUpdate();
-                    const elements: Children[] = [];
-                    var lastDivider = false;
-
-                    function generateAction(
-                        action: Action,
-                        locale?: string,
-                        disabled?: boolean,
-                        tip?: Children
-                    ) {
-                        lastDivider = false;
-                        elements.push(
-                            <MenuItem data={action} disabled={disabled}>
-                                <Text
-                                    id={`app.context_menu.${locale ??
-                                        action.action}`}
-                                />
-                                { tip && <div className="tip">
-                                    { tip }
-                                </div> }
-                            </MenuItem>
-                        );
-                    }
-
-                    function pushDivider() {
-                        if (lastDivider || elements.length === 0) return;
-                        lastDivider = true;
-                        elements.push(<LineDivider />);
-                    }
-
-                    if (server_list) {
-                        let server = useServer(server_list, forceUpdate);
-                        let permissions = useServerPermission(server_list, forceUpdate);
-                        if (server) {
-                            if (permissions & ServerPermission.ManageChannels) generateAction({ action: 'create_channel', target: server });
-                            if (permissions & ServerPermission.ManageServer) generateAction({ action: 'open_server_settings', id: server_list });
-                        }
-
-                        return elements;
-                    }
-
-                    if (document.getSelection()?.toString().length ?? 0 > 0) {
-                        generateAction({ action: "copy_selection" }, undefined, undefined, <Text id="shortcuts.ctrlc" />);
-                        pushDivider();
-                    }
-
-                    const channel = useChannel(cid, forceUpdate);
-                    const contextualChannel = useChannel(cxid, forceUpdate);
-                    const targetChannel = channel ?? contextualChannel;
-
-                    const user = useUser(uid, forceUpdate);
-                    const serverChannel = targetChannel && (targetChannel.channel_type === 'TextChannel' || targetChannel.channel_type === 'VoiceChannel') ? targetChannel : undefined;
-                    const server = useServer(serverChannel ? serverChannel.server : sid, forceUpdate);
-
-                    const channelPermissions = targetChannel ? useChannelPermission(targetChannel._id, forceUpdate) : 0;
-                    const serverPermissions = server ? useServerPermission(server._id, forceUpdate) : (
-                        serverChannel ? useServerPermission(serverChannel.server, forceUpdate) : 0
-                    );
-                    const userPermissions = user ? useUserPermission(user._id, forceUpdate) : 0;
-
-                    if (channel && unread) {
-                        generateAction({ action: "mark_as_read", channel });
-                    }
-
-                    if (contextualChannel) {
-                        if (user && user._id !== userId) {
-                            generateAction({
-                                action: "mention",
-                                user: user._id
-                            });
-
-                            pushDivider();
-                        }
-                    }
-
-                    if (user) {
-                        let actions: Action['action'][];
-                        switch (user.relationship) {
-                            case Users.Relationship.User: actions = []; break;
-                            case Users.Relationship.Friend:
-                                actions = [
-                                    "remove_friend",
-                                    "block_user"
-                                ];
-                                break;
-                            case Users.Relationship.Incoming:
-                                actions = ["add_friend", "cancel_friend", "block_user"];
-                                break;
-                            case Users.Relationship.Outgoing:
-                                actions = ["cancel_friend", "block_user"];
-                                break;
-                            case Users.Relationship.Blocked:
-                                actions = ["unblock_user"];
-                                break;
-                            case Users.Relationship.BlockedOther:
-                                actions = ["block_user"];
-                                break;
-                            case Users.Relationship.None:
-                            default:
-                                actions = ["add_friend", "block_user"];
-                        }
-
-                        if (userPermissions & UserPermission.ViewProfile) {
-                            generateAction({ action: 'view_profile', user: user._id });
-                        }
-
-                        if (user._id !== userId && userPermissions & UserPermission.SendMessage) {
-                            generateAction({ action: 'message_user', user: user._id });
-                        }
-
-                        for(let i = 0; i < actions.length; i++) {
-                            // The any here is because typescript can't determine that user the actions are linked together correctly
-                            generateAction({ action: actions[i] as any, user })
-                        }
-                    }
-
-                    if (contextualChannel) {
-                        if (contextualChannel.channel_type === "Group" && uid) {
-                            if (
-                                contextualChannel.owner === userId &&
-                                userId !== uid
-                            ) {
-                                generateAction({
-                                    action: "remove_member",
-                                    channel: contextualChannel._id,
-                                    user: uid
-                                });
-                            }
-                        }
-
-                        if (server && uid && userId !== uid && uid !== server.owner) {
-                            if (serverPermissions & ServerPermission.KickMembers)
-                                generateAction({ action: "kick_member", target: server, user: uid });
-                            
-                            if (serverPermissions & ServerPermission.BanMembers)
-                                generateAction({ action: "ban_member", target: server, user: uid });
-                        }
-                    }
-
-                    if (queued) {
-                        generateAction({
-                            action: "retry_message",
-                            message: queued
-                        });
-
-                        generateAction({
-                            action: "cancel_message",
-                            message: queued
-                        });
-                    }
-
-                    if (message && !queued) {
-                        generateAction({
-                            action: "reply_message",
-                            id: message._id
-                        });
-                        
-                        if (
-                            typeof message.content === "string" &&
-                            message.content.length > 0
-                        ) {
-                            generateAction({
-                                action: "quote_message",
-                                content: message.content
-                            });
-
-                            generateAction({
-                                action: "copy_text",
-                                content: message.content
-                            });
-                        }
-
-                        if (message.author === userId) {
-                            generateAction({
-                                action: "edit_message",
-                                id: message._id
-                            });
-                        }
-
-                        if (message.author === userId ||
-                            channelPermissions & ChannelPermission.ManageMessages) {
-                            generateAction({
-                                action: "delete_message",
-                                target: message
-                            });
-                        }
-
-                        if (message.attachments) {
-                            pushDivider();
-                            const { metadata } = message.attachments[0];
-                            const { type } = metadata;
-
-                            generateAction(
-                                {
-                                    action: "open_file",
-                                    attachment: message.attachments[0]
-                                },
-                                type === "Image"
-                                    ? "open_image"
-                                    : type === "Video"
-                                    ? "open_video"
-                                    : "open_file"
-                            );
-
-                            generateAction(
-                                {
-                                    action: "save_file",
-                                    attachment: message.attachments[0]
-                                },
-                                type === "Image"
-                                    ? "save_image"
-                                    : type === "Video"
-                                    ? "save_video"
-                                    : "save_file"
-                            );
-
-                            generateAction(
-                                {
-                                    action: "copy_file_link",
-                                    attachment: message.attachments[0]
-                                },
-                                "copy_link"
-                            );
-                        }
-
-                        if (document.activeElement?.tagName === "A") {
-                            let link = document.activeElement.getAttribute(
-                                "href"
-                            );
-                            if (link) {
-                                pushDivider();
-                                generateAction({ action: "open_link", link });
-                                generateAction({ action: "copy_link", link });
-                            }
-                        }
-                    }
-
-                    let id = sid ?? cid ?? uid ?? message?._id;
-                    if (id) {
-                        pushDivider();
-
-                        if (channel) {
-                            if (channel.channel_type !== 'VoiceChannel') {
-                                generateAction({ action: "open_notification_options", channel }, undefined, undefined, <ChevronRight size={24} />);
-                            }
-
-                            switch (channel.channel_type) {
-                                case 'Group':
-                                    // ! generateAction({ action: "create_invite", target: channel }); FIXME: add support for group invites
-                                    generateAction({ action: "open_channel_settings", id: channel._id }, "open_group_settings");
-                                    generateAction({ action: "leave_group", target: channel }, "leave_group");
-                                    break;
-                                case 'DirectMessage':
-                                    generateAction({ action: "close_dm", target: channel });
-                                    break;
-                                case 'TextChannel':
-                                case 'VoiceChannel':
-                                    // ! FIXME: add permission for invites
-                                    generateAction({ action: "create_invite", target: channel });
-
-                                    if (serverPermissions & ServerPermission.ManageServer)
-                                        generateAction({ action: "open_server_channel_settings", server: channel.server, id: channel._id }, "open_channel_settings");
-                                    
-                                    if (serverPermissions & ServerPermission.ManageChannels)
-                                        generateAction({ action: "delete_channel", target: channel });
-
-                                    break;
-                            }
-                        }
-
-                        if (sid && server) {
-                            if (serverPermissions & ServerPermission.ManageServer)
-                                generateAction({ action: "open_server_settings", id: server._id }, "open_server_settings");
-
-                            if (userId === server.owner) {
-                                generateAction({ action: "delete_server", target: server }, "delete_server");
-                            } else {
-                                generateAction({ action: "leave_server", target: server }, "leave_server");
-                            }
-                        }
-
-                        generateAction(
-                            { action: "copy_id", id },
-                            sid ? "copy_sid" :
-                            cid ? "copy_cid" :
-                            message ? "copy_mid" : "copy_uid"
-                        );
-                    }
-
-                    return elements;
-                }}
-            </ContextMenuWithData>
-            <ContextMenuWithData id="Status" onClose={contextClick} className="Status">
-                {() => <>
-                    <div className="header">
-                        <div className="main">
-                            <div>@{client.user!.username}</div>
-                            <div className="status"><UserStatus user={client.user!} /></div>
-                        </div>
-                        <IconButton>
-                            <MenuItem data={{ action: "open_settings" }}>
-                                <Cog size={18} />
-                            </MenuItem>
-                        </IconButton>
-                    </div>
-                    <LineDivider />
-                    <MenuItem
-                        data={{
-                            action: "set_presence",
-                            presence: Users.Presence.Online
-                        }}
-                        disabled={!isOnline}
-                    >
-                        <div className="indicator online" />
-                        <Text id={`app.status.online`} />
-                    </MenuItem>
-                    <MenuItem
-                        data={{
-                            action: "set_presence",
-                            presence: Users.Presence.Idle
-                        }}
-                        disabled={!isOnline}
-                    >
-                        <div className="indicator idle" />
-                        <Text id={`app.status.idle`} />
-                    </MenuItem>
-                    <MenuItem
-                        data={{
-                            action: "set_presence",
-                            presence: Users.Presence.Busy
-                        }}
-                        disabled={!isOnline}
-                    >
-                        <div className="indicator busy" />
-                        <Text id={`app.status.busy`} />
-                    </MenuItem>
-                    <MenuItem
-                        data={{
-                            action: "set_presence",
-                            presence: Users.Presence.Invisible
-                        }}
-                        disabled={!isOnline}
-                    >
-                        <div className="indicator invisible" />
-                        <Text id={`app.status.invisible`} />
-                    </MenuItem>
-                    <LineDivider />
-                    <div className="header">
-                        <div className="main">
-                            <MenuItem data={{ action: "set_status" }} disabled={!isOnline}>
-                                <Text id={`app.context_menu.custom_status`} />
-                            </MenuItem>
-                        </div>
-                        { client.user!.status?.text && <IconButton>
-                            <MenuItem data={{ action: "clear_status" }}>
-                                <Trash size={18} />
-                            </MenuItem>
-                        </IconButton> }
-                    </div>
-                </>}
-            </ContextMenuWithData>
-            <ContextMenuWithData id="NotificationOptions" onClose={contextClick}>
-                {({ channel }: { channel: Channels.Channel }) => {
-                    const state = props.notifications[channel._id];
-                    const actual = getNotificationState(props.notifications, channel);
-
-                    let elements: Children[] = [
-                        <MenuItem data={{ action: "set_notification_state", key: channel._id }}>
-                            <Text id={`app.main.channel.notifications.default`} />
-                            <div className="tip">
-                                { (state !== undefined) && <Square size={20} /> }
-                                { (state === undefined) && <CheckSquare size={20} /> }
-                            </div>
-                        </MenuItem>
-                    ];
-
-                    function generate(key: string, icon: Children) {
-                        elements.push(
-                            <MenuItem data={{ action: "set_notification_state", key: channel._id, state: key }}>
-                                { icon }
-                                <Text id={`app.main.channel.notifications.${key}`} />
-                                { (state === undefined && actual === key) && <div className="tip"><LeftArrowAlt size={20} /></div> }
-                                { (state === key) && <div className="tip"><Check size={20} /></div> }
-                            </MenuItem>
-                        );
-                    }
-
-                    generate('all', <Bell size={24} />);
-                    generate('mention', <At size={24} />);
-                    generate('muted', <BellOff size={24} />);
-                    generate('none', <Block size={24} />);
-
-                    return elements;
-                }}
-            </ContextMenuWithData>
-        </>
-    );
+	const { openScreen, writeClipboard } = useIntermediate();
+	const client = useContext(AppContext);
+	const userId = client.user!._id;
+	const status = useContext(StatusContext);
+	const isOnline = status === ClientStatus.ONLINE;
+	const history = useHistory();
+
+	function contextClick(data?: Action) {
+		if (typeof data === "undefined") return;
+
+		(async () => {
+			switch (data.action) {
+				case "copy_id":
+					writeClipboard(data.id);
+					break;
+				case "copy_selection":
+					writeClipboard(document.getSelection()?.toString() ?? "");
+					break;
+				case "mark_as_read":
+					{
+						if (
+							data.channel.channel_type === "SavedMessages" ||
+							data.channel.channel_type === "VoiceChannel"
+						)
+							return;
+
+						let message =
+							data.channel.channel_type === "TextChannel"
+								? data.channel.last_message
+								: data.channel.last_message._id;
+						dispatch({
+							type: "UNREADS_MARK_READ",
+							channel: data.channel._id,
+							message,
+						});
+
+						client.req(
+							"PUT",
+							`/channels/${data.channel._id}/ack/${message}` as "/channels/id/ack/id",
+						);
+					}
+					break;
+
+				case "retry_message":
+					{
+						const nonce = data.message.id;
+						const fail = (error: any) =>
+							dispatch({
+								type: "QUEUE_FAIL",
+								nonce,
+								error,
+							});
+
+						client.channels
+							.sendMessage(data.message.channel, {
+								nonce: data.message.id,
+								content: data.message.data.content as string,
+								replies: data.message.data.replies,
+							})
+							.catch(fail);
+
+						dispatch({
+							type: "QUEUE_START",
+							nonce,
+						});
+					}
+					break;
+
+				case "cancel_message":
+					{
+						dispatch({
+							type: "QUEUE_REMOVE",
+							nonce: data.message.id,
+						});
+					}
+					break;
+
+				case "mention":
+					{
+						internalEmit(
+							"MessageBox",
+							"append",
+							`<@${data.user}>`,
+							"mention",
+						);
+					}
+					break;
+
+				case "copy_text":
+					writeClipboard(data.content);
+					break;
+
+				case "reply_message":
+					{
+						internalEmit("ReplyBar", "add", data.id);
+					}
+					break;
+
+				case "quote_message":
+					{
+						internalEmit(
+							"MessageBox",
+							"append",
+							data.content,
+							"quote",
+						);
+					}
+					break;
+
+				case "edit_message":
+					{
+						internalEmit(
+							"MessageRenderer",
+							"edit_message",
+							data.id,
+						);
+					}
+					break;
+
+				case "open_file":
+					{
+						window
+							.open(
+								client.generateFileURL(data.attachment),
+								"_blank",
+							)
+							?.focus();
+					}
+					break;
+
+				case "save_file":
+					{
+						window.open(
+							// ! FIXME: do this from revolt.js
+							client
+								.generateFileURL(data.attachment)
+								?.replace(
+									"attachments",
+									"attachments/download",
+								),
+							"_blank",
+						);
+					}
+					break;
+
+				case "copy_file_link":
+					{
+						const { filename } = data.attachment;
+						writeClipboard(
+							// ! FIXME: do from r.js
+							client.generateFileURL(data.attachment) +
+								`/${encodeURI(filename)}`,
+						);
+					}
+					break;
+
+				case "open_link":
+					{
+						window.open(data.link, "_blank")?.focus();
+					}
+					break;
+
+				case "copy_link":
+					{
+						writeClipboard(data.link);
+					}
+					break;
+
+				case "remove_member":
+					{
+						client.channels.removeMember(data.channel, data.user);
+					}
+					break;
+
+				case "view_profile":
+					openScreen({ id: "profile", user_id: data.user });
+					break;
+
+				case "message_user":
+					{
+						const channel = await client.users.openDM(data.user);
+						if (channel) {
+							history.push(`/channel/${channel._id}`);
+						}
+					}
+					break;
+
+				case "add_friend":
+					{
+						await client.users.addFriend(data.user.username);
+					}
+					break;
+
+				case "block_user":
+					openScreen({
+						id: "special_prompt",
+						type: "block_user",
+						target: data.user,
+					});
+					break;
+				case "unblock_user":
+					await client.users.unblockUser(data.user._id);
+					break;
+				case "remove_friend":
+					openScreen({
+						id: "special_prompt",
+						type: "unfriend_user",
+						target: data.user,
+					});
+					break;
+				case "cancel_friend":
+					await client.users.removeFriend(data.user._id);
+					break;
+
+				case "set_presence":
+					{
+						await client.users.editUser({
+							status: {
+								...client.user?.status,
+								presence: data.presence,
+							},
+						});
+					}
+					break;
+
+				case "set_status":
+					openScreen({
+						id: "special_input",
+						type: "set_custom_status",
+					});
+					break;
+
+				case "clear_status":
+					{
+						let { text, ...status } = client.user?.status ?? {};
+						await client.users.editUser({ status });
+					}
+					break;
+
+				case "leave_group":
+				case "close_dm":
+				case "leave_server":
+				case "delete_channel":
+				case "delete_server":
+				case "delete_message":
+				case "create_channel":
+				case "create_invite":
+					// The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever
+					openScreen({
+						id: "special_prompt",
+						type: data.action,
+						target: data.target as any,
+					});
+					break;
+
+				case "ban_member":
+				case "kick_member":
+					openScreen({
+						id: "special_prompt",
+						type: data.action,
+						target: data.target,
+						user: data.user,
+					});
+					break;
+
+				case "open_notification_options": {
+					openContextMenu("NotificationOptions", {
+						channel: data.channel,
+					});
+					break;
+				}
+
+				case "open_settings":
+					history.push("/settings");
+					break;
+				case "open_channel_settings":
+					history.push(`/channel/${data.id}/settings`);
+					break;
+				case "open_server_channel_settings":
+					history.push(
+						`/server/${data.server}/channel/${data.id}/settings`,
+					);
+					break;
+				case "open_server_settings":
+					history.push(`/server/${data.id}/settings`);
+					break;
+
+				case "set_notification_state": {
+					const { key, state } = data;
+					if (state) {
+						dispatch({ type: "NOTIFICATIONS_SET", key, state });
+					} else {
+						dispatch({ type: "NOTIFICATIONS_REMOVE", key });
+					}
+					break;
+				}
+			}
+		})().catch((err) => {
+			openScreen({ id: "error", error: takeError(err) });
+		});
+	}
+
+	return (
+		<>
+			<ContextMenuWithData id="Menu" onClose={contextClick}>
+				{({
+					user: uid,
+					channel: cid,
+					server: sid,
+					message,
+					server_list,
+					queued,
+					unread,
+					contextualChannel: cxid,
+				}: ContextMenuData) => {
+					const forceUpdate = useForceUpdate();
+					const elements: Children[] = [];
+					var lastDivider = false;
+
+					function generateAction(
+						action: Action,
+						locale?: string,
+						disabled?: boolean,
+						tip?: Children,
+					) {
+						lastDivider = false;
+						elements.push(
+							<MenuItem data={action} disabled={disabled}>
+								<Text
+									id={`app.context_menu.${
+										locale ?? action.action
+									}`}
+								/>
+								{tip && <div className="tip">{tip}</div>}
+							</MenuItem>,
+						);
+					}
+
+					function pushDivider() {
+						if (lastDivider || elements.length === 0) return;
+						lastDivider = true;
+						elements.push(<LineDivider />);
+					}
+
+					if (server_list) {
+						let server = useServer(server_list, forceUpdate);
+						let permissions = useServerPermission(
+							server_list,
+							forceUpdate,
+						);
+						if (server) {
+							if (permissions & ServerPermission.ManageChannels)
+								generateAction({
+									action: "create_channel",
+									target: server,
+								});
+							if (permissions & ServerPermission.ManageServer)
+								generateAction({
+									action: "open_server_settings",
+									id: server_list,
+								});
+						}
+
+						return elements;
+					}
+
+					if (document.getSelection()?.toString().length ?? 0 > 0) {
+						generateAction(
+							{ action: "copy_selection" },
+							undefined,
+							undefined,
+							<Text id="shortcuts.ctrlc" />,
+						);
+						pushDivider();
+					}
+
+					const channel = useChannel(cid, forceUpdate);
+					const contextualChannel = useChannel(cxid, forceUpdate);
+					const targetChannel = channel ?? contextualChannel;
+
+					const user = useUser(uid, forceUpdate);
+					const serverChannel =
+						targetChannel &&
+						(targetChannel.channel_type === "TextChannel" ||
+							targetChannel.channel_type === "VoiceChannel")
+							? targetChannel
+							: undefined;
+					const server = useServer(
+						serverChannel ? serverChannel.server : sid,
+						forceUpdate,
+					);
+
+					const channelPermissions = targetChannel
+						? useChannelPermission(targetChannel._id, forceUpdate)
+						: 0;
+					const serverPermissions = server
+						? useServerPermission(server._id, forceUpdate)
+						: serverChannel
+						? useServerPermission(serverChannel.server, forceUpdate)
+						: 0;
+					const userPermissions = user
+						? useUserPermission(user._id, forceUpdate)
+						: 0;
+
+					if (channel && unread) {
+						generateAction({ action: "mark_as_read", channel });
+					}
+
+					if (contextualChannel) {
+						if (user && user._id !== userId) {
+							generateAction({
+								action: "mention",
+								user: user._id,
+							});
+
+							pushDivider();
+						}
+					}
+
+					if (user) {
+						let actions: Action["action"][];
+						switch (user.relationship) {
+							case Users.Relationship.User:
+								actions = [];
+								break;
+							case Users.Relationship.Friend:
+								actions = ["remove_friend", "block_user"];
+								break;
+							case Users.Relationship.Incoming:
+								actions = [
+									"add_friend",
+									"cancel_friend",
+									"block_user",
+								];
+								break;
+							case Users.Relationship.Outgoing:
+								actions = ["cancel_friend", "block_user"];
+								break;
+							case Users.Relationship.Blocked:
+								actions = ["unblock_user"];
+								break;
+							case Users.Relationship.BlockedOther:
+								actions = ["block_user"];
+								break;
+							case Users.Relationship.None:
+							default:
+								actions = ["add_friend", "block_user"];
+						}
+
+						if (userPermissions & UserPermission.ViewProfile) {
+							generateAction({
+								action: "view_profile",
+								user: user._id,
+							});
+						}
+
+						if (
+							user._id !== userId &&
+							userPermissions & UserPermission.SendMessage
+						) {
+							generateAction({
+								action: "message_user",
+								user: user._id,
+							});
+						}
+
+						for (let i = 0; i < actions.length; i++) {
+							// The any here is because typescript can't determine that user the actions are linked together correctly
+							generateAction({ action: actions[i] as any, user });
+						}
+					}
+
+					if (contextualChannel) {
+						if (contextualChannel.channel_type === "Group" && uid) {
+							if (
+								contextualChannel.owner === userId &&
+								userId !== uid
+							) {
+								generateAction({
+									action: "remove_member",
+									channel: contextualChannel._id,
+									user: uid,
+								});
+							}
+						}
+
+						if (
+							server &&
+							uid &&
+							userId !== uid &&
+							uid !== server.owner
+						) {
+							if (
+								serverPermissions & ServerPermission.KickMembers
+							)
+								generateAction({
+									action: "kick_member",
+									target: server,
+									user: uid,
+								});
+
+							if (serverPermissions & ServerPermission.BanMembers)
+								generateAction({
+									action: "ban_member",
+									target: server,
+									user: uid,
+								});
+						}
+					}
+
+					if (queued) {
+						generateAction({
+							action: "retry_message",
+							message: queued,
+						});
+
+						generateAction({
+							action: "cancel_message",
+							message: queued,
+						});
+					}
+
+					if (message && !queued) {
+						generateAction({
+							action: "reply_message",
+							id: message._id,
+						});
+
+						if (
+							typeof message.content === "string" &&
+							message.content.length > 0
+						) {
+							generateAction({
+								action: "quote_message",
+								content: message.content,
+							});
+
+							generateAction({
+								action: "copy_text",
+								content: message.content,
+							});
+						}
+
+						if (message.author === userId) {
+							generateAction({
+								action: "edit_message",
+								id: message._id,
+							});
+						}
+
+						if (
+							message.author === userId ||
+							channelPermissions &
+								ChannelPermission.ManageMessages
+						) {
+							generateAction({
+								action: "delete_message",
+								target: message,
+							});
+						}
+
+						if (message.attachments) {
+							pushDivider();
+							const { metadata } = message.attachments[0];
+							const { type } = metadata;
+
+							generateAction(
+								{
+									action: "open_file",
+									attachment: message.attachments[0],
+								},
+								type === "Image"
+									? "open_image"
+									: type === "Video"
+									? "open_video"
+									: "open_file",
+							);
+
+							generateAction(
+								{
+									action: "save_file",
+									attachment: message.attachments[0],
+								},
+								type === "Image"
+									? "save_image"
+									: type === "Video"
+									? "save_video"
+									: "save_file",
+							);
+
+							generateAction(
+								{
+									action: "copy_file_link",
+									attachment: message.attachments[0],
+								},
+								"copy_link",
+							);
+						}
+
+						if (document.activeElement?.tagName === "A") {
+							let link =
+								document.activeElement.getAttribute("href");
+							if (link) {
+								pushDivider();
+								generateAction({ action: "open_link", link });
+								generateAction({ action: "copy_link", link });
+							}
+						}
+					}
+
+					let id = sid ?? cid ?? uid ?? message?._id;
+					if (id) {
+						pushDivider();
+
+						if (channel) {
+							if (channel.channel_type !== "VoiceChannel") {
+								generateAction(
+									{
+										action: "open_notification_options",
+										channel,
+									},
+									undefined,
+									undefined,
+									<ChevronRight size={24} />,
+								);
+							}
+
+							switch (channel.channel_type) {
+								case "Group":
+									// ! generateAction({ action: "create_invite", target: channel }); FIXME: add support for group invites
+									generateAction(
+										{
+											action: "open_channel_settings",
+											id: channel._id,
+										},
+										"open_group_settings",
+									);
+									generateAction(
+										{
+											action: "leave_group",
+											target: channel,
+										},
+										"leave_group",
+									);
+									break;
+								case "DirectMessage":
+									generateAction({
+										action: "close_dm",
+										target: channel,
+									});
+									break;
+								case "TextChannel":
+								case "VoiceChannel":
+									// ! FIXME: add permission for invites
+									generateAction({
+										action: "create_invite",
+										target: channel,
+									});
+
+									if (
+										serverPermissions &
+										ServerPermission.ManageServer
+									)
+										generateAction(
+											{
+												action: "open_server_channel_settings",
+												server: channel.server,
+												id: channel._id,
+											},
+											"open_channel_settings",
+										);
+
+									if (
+										serverPermissions &
+										ServerPermission.ManageChannels
+									)
+										generateAction({
+											action: "delete_channel",
+											target: channel,
+										});
+
+									break;
+							}
+						}
+
+						if (sid && server) {
+							if (
+								serverPermissions &
+								ServerPermission.ManageServer
+							)
+								generateAction(
+									{
+										action: "open_server_settings",
+										id: server._id,
+									},
+									"open_server_settings",
+								);
+
+							if (userId === server.owner) {
+								generateAction(
+									{ action: "delete_server", target: server },
+									"delete_server",
+								);
+							} else {
+								generateAction(
+									{ action: "leave_server", target: server },
+									"leave_server",
+								);
+							}
+						}
+
+						generateAction(
+							{ action: "copy_id", id },
+							sid
+								? "copy_sid"
+								: cid
+								? "copy_cid"
+								: message
+								? "copy_mid"
+								: "copy_uid",
+						);
+					}
+
+					return elements;
+				}}
+			</ContextMenuWithData>
+			<ContextMenuWithData
+				id="Status"
+				onClose={contextClick}
+				className="Status">
+				{() => (
+					<>
+						<div className="header">
+							<div className="main">
+								<div>@{client.user!.username}</div>
+								<div className="status">
+									<UserStatus user={client.user!} />
+								</div>
+							</div>
+							<IconButton>
+								<MenuItem data={{ action: "open_settings" }}>
+									<Cog size={18} />
+								</MenuItem>
+							</IconButton>
+						</div>
+						<LineDivider />
+						<MenuItem
+							data={{
+								action: "set_presence",
+								presence: Users.Presence.Online,
+							}}
+							disabled={!isOnline}>
+							<div className="indicator online" />
+							<Text id={`app.status.online`} />
+						</MenuItem>
+						<MenuItem
+							data={{
+								action: "set_presence",
+								presence: Users.Presence.Idle,
+							}}
+							disabled={!isOnline}>
+							<div className="indicator idle" />
+							<Text id={`app.status.idle`} />
+						</MenuItem>
+						<MenuItem
+							data={{
+								action: "set_presence",
+								presence: Users.Presence.Busy,
+							}}
+							disabled={!isOnline}>
+							<div className="indicator busy" />
+							<Text id={`app.status.busy`} />
+						</MenuItem>
+						<MenuItem
+							data={{
+								action: "set_presence",
+								presence: Users.Presence.Invisible,
+							}}
+							disabled={!isOnline}>
+							<div className="indicator invisible" />
+							<Text id={`app.status.invisible`} />
+						</MenuItem>
+						<LineDivider />
+						<div className="header">
+							<div className="main">
+								<MenuItem
+									data={{ action: "set_status" }}
+									disabled={!isOnline}>
+									<Text
+										id={`app.context_menu.custom_status`}
+									/>
+								</MenuItem>
+							</div>
+							{client.user!.status?.text && (
+								<IconButton>
+									<MenuItem data={{ action: "clear_status" }}>
+										<Trash size={18} />
+									</MenuItem>
+								</IconButton>
+							)}
+						</div>
+					</>
+				)}
+			</ContextMenuWithData>
+			<ContextMenuWithData
+				id="NotificationOptions"
+				onClose={contextClick}>
+				{({ channel }: { channel: Channels.Channel }) => {
+					const state = props.notifications[channel._id];
+					const actual = getNotificationState(
+						props.notifications,
+						channel,
+					);
+
+					let elements: Children[] = [
+						<MenuItem
+							data={{
+								action: "set_notification_state",
+								key: channel._id,
+							}}>
+							<Text
+								id={`app.main.channel.notifications.default`}
+							/>
+							<div className="tip">
+								{state !== undefined && <Square size={20} />}
+								{state === undefined && (
+									<CheckSquare size={20} />
+								)}
+							</div>
+						</MenuItem>,
+					];
+
+					function generate(key: string, icon: Children) {
+						elements.push(
+							<MenuItem
+								data={{
+									action: "set_notification_state",
+									key: channel._id,
+									state: key,
+								}}>
+								{icon}
+								<Text
+									id={`app.main.channel.notifications.${key}`}
+								/>
+								{state === undefined && actual === key && (
+									<div className="tip">
+										<LeftArrowAlt size={20} />
+									</div>
+								)}
+								{state === key && (
+									<div className="tip">
+										<Check size={20} />
+									</div>
+								)}
+							</MenuItem>,
+						);
+					}
+
+					generate("all", <Bell size={24} />);
+					generate("mention", <At size={24} />);
+					generate("muted", <BellOff size={24} />);
+					generate("none", <Block size={24} />);
+
+					return elements;
+				}}
+			</ContextMenuWithData>
+		</>
+	);
 }
 
-export default connectState(
-    ContextMenus,
-    state => {
-        return {
-            notifications: state.notifications
-        };
-    }
-);
+export default connectState(ContextMenus, (state) => {
+	return {
+		notifications: state.notifications,
+	};
+});
diff --git a/src/lib/PaintCounter.tsx b/src/lib/PaintCounter.tsx
index 487a015c08de2964d20e405c5cc014d75ea27180..15cd5c37c9d0b6324ca056e5f8404c803a3d4d3e 100644
--- a/src/lib/PaintCounter.tsx
+++ b/src/lib/PaintCounter.tsx
@@ -2,17 +2,21 @@ import { useState } from "preact/hooks";
 
 const counts: { [key: string]: number } = {};
 
-export default function PaintCounter({ small, always }: { small?: boolean, always?: boolean }) {
-    if (import.meta.env.PROD && !always) return null;
+export default function PaintCounter({
+	small,
+	always,
+}: {
+	small?: boolean;
+	always?: boolean;
+}) {
+	if (import.meta.env.PROD && !always) return null;
 
-    const [uniqueId] = useState('' + Math.random());
-    const count = counts[uniqueId] ?? 0;
-    counts[uniqueId] = count + 1;
-    return (
-        <div style={{ textAlign: 'center', fontSize: '0.8em' }}>
-            { small ? <>P: { count + 1 }</> : <>
-                Painted {count + 1} time(s).
-            </> }
-        </div>
-    )
+	const [uniqueId] = useState("" + Math.random());
+	const count = counts[uniqueId] ?? 0;
+	counts[uniqueId] = count + 1;
+	return (
+		<div style={{ textAlign: "center", fontSize: "0.8em" }}>
+			{small ? <>P: {count + 1}</> : <>Painted {count + 1} time(s).</>}
+		</div>
+	);
 }
diff --git a/src/lib/TextAreaAutoSize.tsx b/src/lib/TextAreaAutoSize.tsx
index c602655489632aa84eee5c96aff38d16dbccb220..a01cc5757b286e4f23d552bfe043cc5ad2151f3a 100644
--- a/src/lib/TextAreaAutoSize.tsx
+++ b/src/lib/TextAreaAutoSize.tsx
@@ -1,80 +1,113 @@
 import { useEffect, useRef } from "preact/hooks";
+
+import TextArea, {
+	DEFAULT_LINE_HEIGHT,
+	DEFAULT_TEXT_AREA_PADDING,
+	TextAreaProps,
+	TEXT_AREA_BORDER_WIDTH,
+} from "../components/ui/TextArea";
+
 import { internalSubscribe } from "./eventEmitter";
 import { isTouchscreenDevice } from "./isTouchscreenDevice";
-import TextArea, { DEFAULT_LINE_HEIGHT, DEFAULT_TEXT_AREA_PADDING, TextAreaProps, TEXT_AREA_BORDER_WIDTH } from "../components/ui/TextArea";
 
-type TextAreaAutoSizeProps = Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'style' | 'value'> & TextAreaProps & {
-    forceFocus?: boolean
-    autoFocus?: boolean
-    minHeight?: number
-    maxRows?: number
-    value: string
+type TextAreaAutoSizeProps = Omit<
+	JSX.HTMLAttributes<HTMLTextAreaElement>,
+	"style" | "value"
+> &
+	TextAreaProps & {
+		forceFocus?: boolean;
+		autoFocus?: boolean;
+		minHeight?: number;
+		maxRows?: number;
+		value: string;
 
-    id?: string
-};
+		id?: string;
+	};
 
 export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
-    const { autoFocus, minHeight, maxRows, value, padding, lineHeight, hideBorder, forceFocus, children, as, ...textAreaProps } = props;
-    const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
-
-    const heightPadding = ((padding ?? DEFAULT_TEXT_AREA_PADDING) + (hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) * 2;
-    const height = Math.max(Math.min(value.split('\n').length, maxRows ?? Infinity) * line + heightPadding, minHeight ?? 0);
-
-    const ref = useRef<HTMLTextAreaElement>();
-
-    useEffect(() => {
-        if (isTouchscreenDevice) return;
-        autoFocus && ref.current.focus();
-    }, [value]);
-    
-    const inputSelected = () =>
-        ["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
-
-    useEffect(() => {
-        if (forceFocus) {
-            ref.current.focus();
-        }
-
-        if (isTouchscreenDevice) return;
-        if (autoFocus && !inputSelected()) {
-            ref.current.focus();
-        }
-
-        // ? if you are wondering what this is
-        // ? it is a quick and dirty hack to fix
-        // ? value not setting correctly
-        // ? I have no clue what's going on
-        ref.current.value = value;
-
-        if (!autoFocus) return;
-        function keyDown(e: KeyboardEvent) {
-            if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
-            if (e.key.length !== 1) return;
-            if (ref && !inputSelected()) {
-                ref.current.focus();
-            }
-        }
-
-        document.body.addEventListener("keydown", keyDown);
-        return () => document.body.removeEventListener("keydown", keyDown);
-    }, [ref]);
-
-    useEffect(() => {
-        function focus(id: string) {
-            if (id === props.id) {
-                ref.current.focus();
-            }
-        }
-
-        return internalSubscribe("TextArea", "focus", focus);
-    }, [ref]);
-
-    return <TextArea
-        ref={ref}
-        value={value}
-        padding={padding}
-        style={{ height }}
-        hideBorder={hideBorder}
-        lineHeight={lineHeight}
-        {...textAreaProps} />;
+	const {
+		autoFocus,
+		minHeight,
+		maxRows,
+		value,
+		padding,
+		lineHeight,
+		hideBorder,
+		forceFocus,
+		children,
+		as,
+		...textAreaProps
+	} = props;
+	const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
+
+	const heightPadding =
+		((padding ?? DEFAULT_TEXT_AREA_PADDING) +
+			(hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) *
+		2;
+	const height = Math.max(
+		Math.min(value.split("\n").length, maxRows ?? Infinity) * line +
+			heightPadding,
+		minHeight ?? 0,
+	);
+
+	const ref = useRef<HTMLTextAreaElement>();
+
+	useEffect(() => {
+		if (isTouchscreenDevice) return;
+		autoFocus && ref.current.focus();
+	}, [value]);
+
+	const inputSelected = () =>
+		["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
+
+	useEffect(() => {
+		if (forceFocus) {
+			ref.current.focus();
+		}
+
+		if (isTouchscreenDevice) return;
+		if (autoFocus && !inputSelected()) {
+			ref.current.focus();
+		}
+
+		// ? if you are wondering what this is
+		// ? it is a quick and dirty hack to fix
+		// ? value not setting correctly
+		// ? I have no clue what's going on
+		ref.current.value = value;
+
+		if (!autoFocus) return;
+		function keyDown(e: KeyboardEvent) {
+			if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
+			if (e.key.length !== 1) return;
+			if (ref && !inputSelected()) {
+				ref.current.focus();
+			}
+		}
+
+		document.body.addEventListener("keydown", keyDown);
+		return () => document.body.removeEventListener("keydown", keyDown);
+	}, [ref]);
+
+	useEffect(() => {
+		function focus(id: string) {
+			if (id === props.id) {
+				ref.current.focus();
+			}
+		}
+
+		return internalSubscribe("TextArea", "focus", focus);
+	}, [ref]);
+
+	return (
+		<TextArea
+			ref={ref}
+			value={value}
+			padding={padding}
+			style={{ height }}
+			hideBorder={hideBorder}
+			lineHeight={lineHeight}
+			{...textAreaProps}
+		/>
+	);
 }
diff --git a/src/lib/conversion.ts b/src/lib/conversion.ts
index 61dcad9d378514732edc34c7111d330bf81d33c2..263dd5b119907fc81b7f633465e50add510cd062 100644
--- a/src/lib/conversion.ts
+++ b/src/lib/conversion.ts
@@ -1,9 +1,9 @@
 export function urlBase64ToUint8Array(base64String: string) {
-    const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
-    const base64 = (base64String + padding)
-        .replace(/\-/g, "+")
-        .replace(/_/g, "/");
-    const rawData = window.atob(base64);
+	const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
+	const base64 = (base64String + padding)
+		.replace(/\-/g, "+")
+		.replace(/_/g, "/");
+	const rawData = window.atob(base64);
 
-    return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
+	return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
 }
diff --git a/src/lib/debounce.ts b/src/lib/debounce.ts
index b65292bd4486ebac17ca8dfbd484652f68463849..2b7d63ae3544a65c71ab8abdd227711187b91650 100644
--- a/src/lib/debounce.ts
+++ b/src/lib/debounce.ts
@@ -1,15 +1,15 @@
 export function debounce(cb: Function, duration: number) {
-    // Store the timer variable.
-    let timer: NodeJS.Timeout;
-    // This function is given to React.
-    return (...args: any[]) => {
-        // Get rid of the old timer.
-        clearTimeout(timer);
-        // Set a new timer.
-        timer = setTimeout(() => {
-            // Instead calling the new function.
-            // (with the newer data)
-            cb(...args);
-        }, duration);
-    };
+	// Store the timer variable.
+	let timer: NodeJS.Timeout;
+	// This function is given to React.
+	return (...args: any[]) => {
+		// Get rid of the old timer.
+		clearTimeout(timer);
+		// Set a new timer.
+		timer = setTimeout(() => {
+			// Instead calling the new function.
+			// (with the newer data)
+			cb(...args);
+		}, duration);
+	};
 }
diff --git a/src/lib/eventEmitter.ts b/src/lib/eventEmitter.ts
index ae0ae52e89ecb75cf3e466503bbe2f8aa50ddfc3..131f1bf75098eed1571e4238bdaa2f4c3790a96c 100644
--- a/src/lib/eventEmitter.ts
+++ b/src/lib/eventEmitter.ts
@@ -1,13 +1,18 @@
 import EventEmitter from "eventemitter3";
+
 export const InternalEvent = new EventEmitter();
 
-export function internalSubscribe(ns: string, event: string, fn: (...args: any[]) => void) {
-    InternalEvent.addListener(ns + '/' + event, fn);
-    return () => InternalEvent.removeListener(ns + '/' + event, fn);
+export function internalSubscribe(
+	ns: string,
+	event: string,
+	fn: (...args: any[]) => void,
+) {
+	InternalEvent.addListener(ns + "/" + event, fn);
+	return () => InternalEvent.removeListener(ns + "/" + event, fn);
 }
 
 export function internalEmit(ns: string, event: string, ...args: any[]) {
-    InternalEvent.emit(ns + '/' + event, ...args);
+	InternalEvent.emit(ns + "/" + event, ...args);
 }
 
 // Event structure: namespace/event
diff --git a/src/lib/fileSize.ts b/src/lib/fileSize.ts
index c6bba81d55577507c59070adb713ef186bf28acd..9caea0cf734b758252bd615bdf53286b42e9041b 100644
--- a/src/lib/fileSize.ts
+++ b/src/lib/fileSize.ts
@@ -1,9 +1,9 @@
 export function determineFileSize(size: number) {
-    if (size > 1e6) {
-        return `${(size / 1e6).toFixed(2)} MB`;
-    } else if (size > 1e3) {
-        return `${(size / 1e3).toFixed(2)} KB`;
-    }
+	if (size > 1e6) {
+		return `${(size / 1e6).toFixed(2)} MB`;
+	} else if (size > 1e3) {
+		return `${(size / 1e3).toFixed(2)} KB`;
+	}
 
-    return `${size} B`;
+	return `${size} B`;
 }
diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx
index a58dff14a608828cac37490602356f021122efe8..e8ca877773bd43774c91194e9bf3244bc5bf87ac 100644
--- a/src/lib/i18n.tsx
+++ b/src/lib/i18n.tsx
@@ -1,59 +1,62 @@
 import { IntlContext, translate } from "preact-i18n";
 import { useContext } from "preact/hooks";
+
 import { Children } from "../types/Preact";
 
 interface Fields {
-    [key: string]: Children
+	[key: string]: Children;
 }
 
 interface Props {
-    id: string;
-    fields: Fields
+	id: string;
+	fields: Fields;
 }
 
 export interface IntlType {
-    intl: {
-        dictionary: {
-            [key: string]: Object | string
-        }
-    }
+	intl: {
+		dictionary: {
+			[key: string]: Object | string;
+		};
+	};
 }
 
 // This will exhibit O(2^n) behaviour.
 function recursiveReplaceFields(input: string, fields: Fields) {
-    const key = Object.keys(fields)[0];
-    if (key) {
-        const { [key]: field, ...restOfFields } = fields;
-        if (typeof field === 'undefined') return [ input ];
-
-        const values: (Children | string[])[] = input.split(`{{${key}}}`)
-            .map(v => recursiveReplaceFields(v, restOfFields));
-
-        for (let i=values.length - 1;i>0;i-=2) {
-            values.splice(i, 0, field);
-        }
-        
-        return values.flat();
-    } else {
-        // base case
-        return [ input ];
-    }
+	const key = Object.keys(fields)[0];
+	if (key) {
+		const { [key]: field, ...restOfFields } = fields;
+		if (typeof field === "undefined") return [input];
+
+		const values: (Children | string[])[] = input
+			.split(`{{${key}}}`)
+			.map((v) => recursiveReplaceFields(v, restOfFields));
+
+		for (let i = values.length - 1; i > 0; i -= 2) {
+			values.splice(i, 0, field);
+		}
+
+		return values.flat();
+	} else {
+		// base case
+		return [input];
+	}
 }
 
 export function TextReact({ id, fields }: Props) {
-    const { intl } = useContext(IntlContext) as unknown as IntlType;
+	const { intl } = useContext(IntlContext) as unknown as IntlType;
 
-    const path = id.split('.');
-    let entry = intl.dictionary[path.shift()!];
-    for (let key of path) {
-        // @ts-expect-error
-        entry = entry[key];
-    }
+	const path = id.split(".");
+	let entry = intl.dictionary[path.shift()!];
+	for (let key of path) {
+		// @ts-expect-error
+		entry = entry[key];
+	}
 
-    return <>{ recursiveReplaceFields(entry as string, fields) }</>;
+	return <>{recursiveReplaceFields(entry as string, fields)}</>;
 }
 
 export function useTranslation() {
-    const { intl } = useContext(IntlContext) as unknown as IntlType;
-    return (id: string, fields?: Object, plural?: number, fallback?: string) => translate(id, "", intl.dictionary, fields, plural, fallback);
+	const { intl } = useContext(IntlContext) as unknown as IntlType;
+	return (id: string, fields?: Object, plural?: number, fallback?: string) =>
+		translate(id, "", intl.dictionary, fields, plural, fallback);
 }
diff --git a/src/lib/isTouchscreenDevice.ts b/src/lib/isTouchscreenDevice.ts
index a91bb23b978a22595880fc9f92bd5946d7750418..6f2dc0ab713b2009a202ce142b0c89cc568d1705 100644
--- a/src/lib/isTouchscreenDevice.ts
+++ b/src/lib/isTouchscreenDevice.ts
@@ -1,7 +1,8 @@
 import { isDesktop, isMobile, isTablet } from "react-device-detect";
+
 export const isTouchscreenDevice =
-    isDesktop && !isTablet
-        ? false
-        : (typeof window !== "undefined"
-              ? navigator.maxTouchPoints > 0
-              : false) || isMobile;
+	isDesktop && !isTablet
+		? false
+		: (typeof window !== "undefined"
+				? navigator.maxTouchPoints > 0
+				: false) || isMobile;
diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts
index da4acadbe0f3694499d8fd2a21ca1c3b6c0cb78a..fd71d194996f17a6159562f48afafbfbf17d3591 100644
--- a/src/lib/renderer/Singleton.ts
+++ b/src/lib/renderer/Singleton.ts
@@ -1,192 +1,206 @@
-import { RendererRoutines, RenderState, ScrollState } from "./types";
-import { SimpleRenderer } from "./simple/SimpleRenderer";
-import { useEffect, useState } from "preact/hooks";
-import EventEmitter3 from 'eventemitter3';
+import EventEmitter3 from "eventemitter3";
 import { Client, Message } from "revolt.js";
 
+import { useEffect, useState } from "preact/hooks";
+
+import { SimpleRenderer } from "./simple/SimpleRenderer";
+import { RendererRoutines, RenderState, ScrollState } from "./types";
+
 export const SMOOTH_SCROLL_ON_RECEIVE = false;
 
 export class SingletonRenderer extends EventEmitter3 {
-    client?: Client;
-    channel?: string;
-    state: RenderState;
-    currentRenderer: RendererRoutines;
-
-    stale = false;
-    fetchingTop = false;
-    fetchingBottom = false;
-
-    constructor() {
-        super();
-
-        this.receive = this.receive.bind(this);
-        this.edit = this.edit.bind(this);
-        this.delete = this.delete.bind(this);
-
-        this.state = { type: 'LOADING' };
-        this.currentRenderer = SimpleRenderer;
-    }
-
-    private receive(message: Message) {
-        this.currentRenderer.receive(this, message);
-    }
-
-    private edit(id: string, patch: Partial<Message>) {
-        this.currentRenderer.edit(this, id, patch);
-    }
-
-    private delete(id: string) {
-        this.currentRenderer.delete(this, id);
-    }
-
-    subscribe(client: Client) {
-        if (this.client) {
-            this.client.removeListener('message', this.receive);
-            this.client.removeListener('message/update', this.edit);
-            this.client.removeListener('message/delete', this.delete);
-        }
-
-        this.client = client;
-        client.addListener('message', this.receive);
-        client.addListener('message/update', this.edit);
-        client.addListener('message/delete', this.delete);
-    }
-
-    private setStateUnguarded(state: RenderState, scroll?: ScrollState) {
-        this.state = state;
-        this.emit('state', state);
-
-        if (scroll) {
-            this.emit('scroll', scroll);
-        }
-    }
-
-    setState(id: string, state: RenderState, scroll?: ScrollState) {
-        if (id !== this.channel) return;
-        this.setStateUnguarded(state, scroll);
-    }
-
-    markStale() {
-        this.stale = true;   
-    }
-
-    async init(id: string) {
-        this.channel = id;
-        this.stale = false;
-        this.setStateUnguarded({ type: 'LOADING' });
-        await this.currentRenderer.init(this, id);
-    }
-
-    async reloadStale(id: string) {
-        if (this.stale) {
-            this.stale = false;
-            await this.init(id);
-        }
-    }
-
-    async loadTop(ref?: HTMLDivElement) {
-        if (this.fetchingTop) return;
-        this.fetchingTop = true;
-
-        function generateScroll(end: string): ScrollState {
-            if (ref) {
-                let heightRemoved = 0;
-                let messageContainer = ref.children[0];
-                if (messageContainer) {
-                    for (let child of Array.from(messageContainer.children)) {
-                        // If this child has a ulid.
-                        if (child.id?.length === 26) {
-                            // Check whether it was removed.
-                            if (child.id.localeCompare(end) === 1) {
-                                heightRemoved += child.clientHeight +
-                                    // We also need to take into account the top margin of the container.
-                                    parseInt(window.getComputedStyle(child).marginTop.slice(0, -2));
-                            }
-                        }
-                    }
-                }
-
-                return {
-                    type: 'OffsetTop',
-                    previousHeight: ref.scrollHeight - heightRemoved
-                }
-            } else {
-                return {
-                    type: 'OffsetTop',
-                    previousHeight: 0
-                }
-            }
-        }
-
-        await this.currentRenderer.loadTop(this, generateScroll);
-
-        // Allow state updates to propagate.
-        setTimeout(() => this.fetchingTop = false, 0);
-    }
-
-    async loadBottom(ref?: HTMLDivElement) {
-        if (this.fetchingBottom) return;
-        this.fetchingBottom = true;
-
-        function generateScroll(start: string): ScrollState {
-            if (ref) {
-                let heightRemoved = 0;
-                let messageContainer = ref.children[0];
-                if (messageContainer) {
-                    for (let child of Array.from(messageContainer.children)) {
-                        // If this child has a ulid.
-                        if (child.id?.length === 26) {
-                            // Check whether it was removed.
-                            if (child.id.localeCompare(start) === -1) {
-                                heightRemoved += child.clientHeight +
-                                    // We also need to take into account the top margin of the container.
-                                    parseInt(window.getComputedStyle(child).marginTop.slice(0, -2));
-                            }
-                        }
-                    }
-                }
-
-                return {
-                    type: 'ScrollTop',
-                    y: ref.scrollTop - heightRemoved
-                }
-            } else {
-                return {
-                    type: 'ScrollToBottom'
-                }
-            }
-        }
-
-        await this.currentRenderer.loadBottom(this, generateScroll);
-
-        // Allow state updates to propagate.
-        setTimeout(() => this.fetchingBottom = false, 0);
-    }
-
-    async jumpToBottom(id: string, smooth: boolean) {
-        if (id !== this.channel) return;
-        if (this.state.type === 'RENDER' && this.state.atBottom) {
-            this.emit('scroll', { type: 'ScrollToBottom', smooth });
-        } else {
-            await this.currentRenderer.init(this, id, true);
-        }
-    }
+	client?: Client;
+	channel?: string;
+	state: RenderState;
+	currentRenderer: RendererRoutines;
+
+	stale = false;
+	fetchingTop = false;
+	fetchingBottom = false;
+
+	constructor() {
+		super();
+
+		this.receive = this.receive.bind(this);
+		this.edit = this.edit.bind(this);
+		this.delete = this.delete.bind(this);
+
+		this.state = { type: "LOADING" };
+		this.currentRenderer = SimpleRenderer;
+	}
+
+	private receive(message: Message) {
+		this.currentRenderer.receive(this, message);
+	}
+
+	private edit(id: string, patch: Partial<Message>) {
+		this.currentRenderer.edit(this, id, patch);
+	}
+
+	private delete(id: string) {
+		this.currentRenderer.delete(this, id);
+	}
+
+	subscribe(client: Client) {
+		if (this.client) {
+			this.client.removeListener("message", this.receive);
+			this.client.removeListener("message/update", this.edit);
+			this.client.removeListener("message/delete", this.delete);
+		}
+
+		this.client = client;
+		client.addListener("message", this.receive);
+		client.addListener("message/update", this.edit);
+		client.addListener("message/delete", this.delete);
+	}
+
+	private setStateUnguarded(state: RenderState, scroll?: ScrollState) {
+		this.state = state;
+		this.emit("state", state);
+
+		if (scroll) {
+			this.emit("scroll", scroll);
+		}
+	}
+
+	setState(id: string, state: RenderState, scroll?: ScrollState) {
+		if (id !== this.channel) return;
+		this.setStateUnguarded(state, scroll);
+	}
+
+	markStale() {
+		this.stale = true;
+	}
+
+	async init(id: string) {
+		this.channel = id;
+		this.stale = false;
+		this.setStateUnguarded({ type: "LOADING" });
+		await this.currentRenderer.init(this, id);
+	}
+
+	async reloadStale(id: string) {
+		if (this.stale) {
+			this.stale = false;
+			await this.init(id);
+		}
+	}
+
+	async loadTop(ref?: HTMLDivElement) {
+		if (this.fetchingTop) return;
+		this.fetchingTop = true;
+
+		function generateScroll(end: string): ScrollState {
+			if (ref) {
+				let heightRemoved = 0;
+				let messageContainer = ref.children[0];
+				if (messageContainer) {
+					for (let child of Array.from(messageContainer.children)) {
+						// If this child has a ulid.
+						if (child.id?.length === 26) {
+							// Check whether it was removed.
+							if (child.id.localeCompare(end) === 1) {
+								heightRemoved +=
+									child.clientHeight +
+									// We also need to take into account the top margin of the container.
+									parseInt(
+										window
+											.getComputedStyle(child)
+											.marginTop.slice(0, -2),
+									);
+							}
+						}
+					}
+				}
+
+				return {
+					type: "OffsetTop",
+					previousHeight: ref.scrollHeight - heightRemoved,
+				};
+			} else {
+				return {
+					type: "OffsetTop",
+					previousHeight: 0,
+				};
+			}
+		}
+
+		await this.currentRenderer.loadTop(this, generateScroll);
+
+		// Allow state updates to propagate.
+		setTimeout(() => (this.fetchingTop = false), 0);
+	}
+
+	async loadBottom(ref?: HTMLDivElement) {
+		if (this.fetchingBottom) return;
+		this.fetchingBottom = true;
+
+		function generateScroll(start: string): ScrollState {
+			if (ref) {
+				let heightRemoved = 0;
+				let messageContainer = ref.children[0];
+				if (messageContainer) {
+					for (let child of Array.from(messageContainer.children)) {
+						// If this child has a ulid.
+						if (child.id?.length === 26) {
+							// Check whether it was removed.
+							if (child.id.localeCompare(start) === -1) {
+								heightRemoved +=
+									child.clientHeight +
+									// We also need to take into account the top margin of the container.
+									parseInt(
+										window
+											.getComputedStyle(child)
+											.marginTop.slice(0, -2),
+									);
+							}
+						}
+					}
+				}
+
+				return {
+					type: "ScrollTop",
+					y: ref.scrollTop - heightRemoved,
+				};
+			} else {
+				return {
+					type: "ScrollToBottom",
+				};
+			}
+		}
+
+		await this.currentRenderer.loadBottom(this, generateScroll);
+
+		// Allow state updates to propagate.
+		setTimeout(() => (this.fetchingBottom = false), 0);
+	}
+
+	async jumpToBottom(id: string, smooth: boolean) {
+		if (id !== this.channel) return;
+		if (this.state.type === "RENDER" && this.state.atBottom) {
+			this.emit("scroll", { type: "ScrollToBottom", smooth });
+		} else {
+			await this.currentRenderer.init(this, id, true);
+		}
+	}
 }
 
 export const SingletonMessageRenderer = new SingletonRenderer();
 
 export function useRenderState(id: string) {
-    const [state, setState] = useState<Readonly<RenderState>>(SingletonMessageRenderer.state);
-    if (typeof id === "undefined") return;
+	const [state, setState] = useState<Readonly<RenderState>>(
+		SingletonMessageRenderer.state,
+	);
+	if (typeof id === "undefined") return;
 
-    function render(state: RenderState) {
-        setState(state);
-    }
+	function render(state: RenderState) {
+		setState(state);
+	}
 
-    useEffect(() => {
-        SingletonMessageRenderer.addListener("state", render);
-        return () => SingletonMessageRenderer.removeListener("state", render);
-    }, [id]);
+	useEffect(() => {
+		SingletonMessageRenderer.addListener("state", render);
+		return () => SingletonMessageRenderer.removeListener("state", render);
+	}, [id]);
 
-    return state;
+	return state;
 }
diff --git a/src/lib/renderer/simple/SimpleRenderer.ts b/src/lib/renderer/simple/SimpleRenderer.ts
index afd808886d2c48a878f9d465a861ed32e8be9c4f..ce4da1de32890e6fbc094ee61cf52295b6f8f879 100644
--- a/src/lib/renderer/simple/SimpleRenderer.ts
+++ b/src/lib/renderer/simple/SimpleRenderer.ts
@@ -1,178 +1,183 @@
 import { mapMessage } from "../../../context/revoltjs/util";
+
 import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
 import { RendererRoutines } from "../types";
 
 export const SimpleRenderer: RendererRoutines = {
-    init: async (renderer, id, smooth) => {
-        if (renderer.client!.websocket.connected) {
-            renderer.client!.channels
-                .fetchMessagesWithUsers(id, { }, true)
-                .then(({ messages: data }) => {
-                    data.reverse();
-                    let messages = data.map(x => mapMessage(x));
-                    renderer.setState(
-                        id,
-                        {
-                            type: 'RENDER',
-                            messages,
-                            atTop: data.length < 50,
-                            atBottom: true
-                        },
-                        { type: 'ScrollToBottom', smooth }
-                    );
-                });
-        } else {
-            renderer.setState(id, { type: 'WAITING_FOR_NETWORK' });
-        }
-    },
-    receive: async (renderer, message) => {
-        if (message.channel !== renderer.channel) return;
-        if (renderer.state.type !== 'RENDER') return;
-        if (renderer.state.messages.find(x => x._id === message._id)) return;
-        if (!renderer.state.atBottom) return;
-
-        let messages = [ ...renderer.state.messages, mapMessage(message) ];
-        let atTop = renderer.state.atTop;
-        if (messages.length > 150) {
-            messages = messages.slice(messages.length - 150);
-            atTop = false;
-        }
-
-        renderer.setState(
-            message.channel,
-            {
-                ...renderer.state,
-                messages,
-                atTop
-            },
-            { type: 'StayAtBottom', smooth: SMOOTH_SCROLL_ON_RECEIVE }
-        );
-    },
-    edit: async (renderer, id, patch) => {
-        const channel = renderer.channel;
-        if (!channel) return;
-        if (renderer.state.type !== 'RENDER') return;
-            
-        let messages = [ ...renderer.state.messages ];
-        let index = messages.findIndex(x => x._id === id);
-
-        if (index > -1) {
-            let message = { ...messages[index], ...mapMessage(patch) };
-            messages.splice(index, 1, message);
-
-            renderer.setState(
-                channel,
-                {
-                    ...renderer.state,
-                    messages
-                },
-                { type: 'StayAtBottom' }
-            );
-        }
-    },
-    delete: async (renderer, id) => {
-        const channel = renderer.channel;
-        if (!channel) return;
-        if (renderer.state.type !== 'RENDER') return;
-            
-        let messages = [ ...renderer.state.messages ];
-        let index = messages.findIndex(x => x._id === id);
-
-        if (index > -1) {
-            messages.splice(index, 1);
-
-            renderer.setState(
-                channel,
-                {
-                    ...renderer.state,
-                    messages
-                },
-                { type: 'StayAtBottom' }
-            );
-        }
-    },
-    loadTop: async (renderer, generateScroll) => {
-        const channel = renderer.channel;
-        if (!channel) return;
-
-        const state = renderer.state;
-        if (state.type !== 'RENDER') return;
-        if (state.atTop) return;
-
-        const { messages: data } = await renderer.client!.channels.fetchMessagesWithUsers(channel, {
-            before: state.messages[0]._id
-        }, true);
-
-        if (data.length === 0) {
-            return renderer.setState(
-                channel,
-                {
-                    ...state,
-                    atTop: true
-                }
-            );
-        }
-
-        data.reverse();
-        let messages = [ ...data.map(x => mapMessage(x)), ...state.messages ];
-
-        let atTop = false;
-        if (data.length < 50) {
-            atTop = true;
-        }
-
-        let atBottom = state.atBottom;
-        if (messages.length > 150) {
-            messages = messages.slice(0, 150);
-            atBottom = false;
-        }
-
-        renderer.setState(
-            channel,
-            { ...state, atTop, atBottom, messages },
-            generateScroll(messages[messages.length - 1]._id)
-        );
-    },
-    loadBottom: async (renderer, generateScroll) => {
-        const channel = renderer.channel;
-        if (!channel) return;
-
-        const state = renderer.state;
-        if (state.type !== 'RENDER') return;
-        if (state.atBottom) return;
-
-        const { messages: data } = await renderer.client!.channels.fetchMessagesWithUsers(channel, {
-            after: state.messages[state.messages.length - 1]._id,
-            sort: 'Oldest'
-        }, true);
-
-        if (data.length === 0) {
-            return renderer.setState(
-                channel,
-                {
-                    ...state,
-                    atBottom: true
-                }
-            );
-        }
-
-        let messages = [ ...state.messages, ...data.map(x => mapMessage(x)) ];
-
-        let atBottom = false;
-        if (data.length < 50) {
-            atBottom = true;
-        }
-
-        let atTop = state.atTop;
-        if (messages.length > 150) {
-            messages = messages.slice(messages.length - 150);
-            atTop = false;
-        }
-
-        renderer.setState(
-            channel,
-            { ...state, atTop, atBottom, messages },
-            generateScroll(messages[0]._id)
-        );
-    }
+	init: async (renderer, id, smooth) => {
+		if (renderer.client!.websocket.connected) {
+			renderer
+				.client!.channels.fetchMessagesWithUsers(id, {}, true)
+				.then(({ messages: data }) => {
+					data.reverse();
+					let messages = data.map((x) => mapMessage(x));
+					renderer.setState(
+						id,
+						{
+							type: "RENDER",
+							messages,
+							atTop: data.length < 50,
+							atBottom: true,
+						},
+						{ type: "ScrollToBottom", smooth },
+					);
+				});
+		} else {
+			renderer.setState(id, { type: "WAITING_FOR_NETWORK" });
+		}
+	},
+	receive: async (renderer, message) => {
+		if (message.channel !== renderer.channel) return;
+		if (renderer.state.type !== "RENDER") return;
+		if (renderer.state.messages.find((x) => x._id === message._id)) return;
+		if (!renderer.state.atBottom) return;
+
+		let messages = [...renderer.state.messages, mapMessage(message)];
+		let atTop = renderer.state.atTop;
+		if (messages.length > 150) {
+			messages = messages.slice(messages.length - 150);
+			atTop = false;
+		}
+
+		renderer.setState(
+			message.channel,
+			{
+				...renderer.state,
+				messages,
+				atTop,
+			},
+			{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
+		);
+	},
+	edit: async (renderer, id, patch) => {
+		const channel = renderer.channel;
+		if (!channel) return;
+		if (renderer.state.type !== "RENDER") return;
+
+		let messages = [...renderer.state.messages];
+		let index = messages.findIndex((x) => x._id === id);
+
+		if (index > -1) {
+			let message = { ...messages[index], ...mapMessage(patch) };
+			messages.splice(index, 1, message);
+
+			renderer.setState(
+				channel,
+				{
+					...renderer.state,
+					messages,
+				},
+				{ type: "StayAtBottom" },
+			);
+		}
+	},
+	delete: async (renderer, id) => {
+		const channel = renderer.channel;
+		if (!channel) return;
+		if (renderer.state.type !== "RENDER") return;
+
+		let messages = [...renderer.state.messages];
+		let index = messages.findIndex((x) => x._id === id);
+
+		if (index > -1) {
+			messages.splice(index, 1);
+
+			renderer.setState(
+				channel,
+				{
+					...renderer.state,
+					messages,
+				},
+				{ type: "StayAtBottom" },
+			);
+		}
+	},
+	loadTop: async (renderer, generateScroll) => {
+		const channel = renderer.channel;
+		if (!channel) return;
+
+		const state = renderer.state;
+		if (state.type !== "RENDER") return;
+		if (state.atTop) return;
+
+		const { messages: data } =
+			await renderer.client!.channels.fetchMessagesWithUsers(
+				channel,
+				{
+					before: state.messages[0]._id,
+				},
+				true,
+			);
+
+		if (data.length === 0) {
+			return renderer.setState(channel, {
+				...state,
+				atTop: true,
+			});
+		}
+
+		data.reverse();
+		let messages = [...data.map((x) => mapMessage(x)), ...state.messages];
+
+		let atTop = false;
+		if (data.length < 50) {
+			atTop = true;
+		}
+
+		let atBottom = state.atBottom;
+		if (messages.length > 150) {
+			messages = messages.slice(0, 150);
+			atBottom = false;
+		}
+
+		renderer.setState(
+			channel,
+			{ ...state, atTop, atBottom, messages },
+			generateScroll(messages[messages.length - 1]._id),
+		);
+	},
+	loadBottom: async (renderer, generateScroll) => {
+		const channel = renderer.channel;
+		if (!channel) return;
+
+		const state = renderer.state;
+		if (state.type !== "RENDER") return;
+		if (state.atBottom) return;
+
+		const { messages: data } =
+			await renderer.client!.channels.fetchMessagesWithUsers(
+				channel,
+				{
+					after: state.messages[state.messages.length - 1]._id,
+					sort: "Oldest",
+				},
+				true,
+			);
+
+		if (data.length === 0) {
+			return renderer.setState(channel, {
+				...state,
+				atBottom: true,
+			});
+		}
+
+		let messages = [...state.messages, ...data.map((x) => mapMessage(x))];
+
+		let atBottom = false;
+		if (data.length < 50) {
+			atBottom = true;
+		}
+
+		let atTop = state.atTop;
+		if (messages.length > 150) {
+			messages = messages.slice(messages.length - 150);
+			atTop = false;
+		}
+
+		renderer.setState(
+			channel,
+			{ ...state, atTop, atBottom, messages },
+			generateScroll(messages[0]._id),
+		);
+	},
 };
diff --git a/src/lib/renderer/types.ts b/src/lib/renderer/types.ts
index da6709ff420d013c237f9ecaca0e8c67a7d0e010..29bb682a62f24dd97c0dbe43d3f35baa9e2ee69b 100644
--- a/src/lib/renderer/types.ts
+++ b/src/lib/renderer/types.ts
@@ -1,32 +1,48 @@
 import { Message } from "revolt.js";
-import { SingletonRenderer } from "./Singleton";
+
 import { MessageObject } from "../../context/revoltjs/util";
 
+import { SingletonRenderer } from "./Singleton";
+
 export type ScrollState =
-    | { type: "Free" }
-    | { type: "Bottom", scrollingUntil?: number }
-    | { type: "ScrollToBottom" | "StayAtBottom", smooth?: boolean }
-    | { type: "OffsetTop"; previousHeight: number }
-    | { type: "ScrollTop"; y: number };
+	| { type: "Free" }
+	| { type: "Bottom"; scrollingUntil?: number }
+	| { type: "ScrollToBottom" | "StayAtBottom"; smooth?: boolean }
+	| { type: "OffsetTop"; previousHeight: number }
+	| { type: "ScrollTop"; y: number };
 
 export type RenderState =
-    | {
-            type: "LOADING" | "WAITING_FOR_NETWORK" | "EMPTY";
-        }
-    | {
-            type: "RENDER";
-            atTop: boolean;
-            atBottom: boolean;
-            messages: MessageObject[];
-        };
+	| {
+			type: "LOADING" | "WAITING_FOR_NETWORK" | "EMPTY";
+	  }
+	| {
+			type: "RENDER";
+			atTop: boolean;
+			atBottom: boolean;
+			messages: MessageObject[];
+	  };
 
 export interface RendererRoutines {
-    init: (renderer: SingletonRenderer, id: string, smooth?: boolean) => Promise<void>
-    
-    receive: (renderer: SingletonRenderer, message: Message) => Promise<void>;
-    edit: (renderer: SingletonRenderer, id: string, partial: Partial<Message>) => Promise<void>;
-    delete: (renderer: SingletonRenderer, id: string) => Promise<void>;
+	init: (
+		renderer: SingletonRenderer,
+		id: string,
+		smooth?: boolean,
+	) => Promise<void>;
+
+	receive: (renderer: SingletonRenderer, message: Message) => Promise<void>;
+	edit: (
+		renderer: SingletonRenderer,
+		id: string,
+		partial: Partial<Message>,
+	) => Promise<void>;
+	delete: (renderer: SingletonRenderer, id: string) => Promise<void>;
 
-    loadTop: (renderer: SingletonRenderer, generateScroll: (end: string) => ScrollState) => Promise<void>;
-    loadBottom: (renderer: SingletonRenderer, generateScroll: (start: string) => ScrollState) => Promise<void>;
+	loadTop: (
+		renderer: SingletonRenderer,
+		generateScroll: (end: string) => ScrollState,
+	) => Promise<void>;
+	loadBottom: (
+		renderer: SingletonRenderer,
+		generateScroll: (start: string) => ScrollState,
+	) => Promise<void>;
 }
diff --git a/src/lib/stopPropagation.ts b/src/lib/stopPropagation.ts
index df4d95337e7ea16b311b07137f5f4d7df1fe6cb1..0a74bae8e7528ce56b76457dc622c1c24dfd4bbb 100644
--- a/src/lib/stopPropagation.ts
+++ b/src/lib/stopPropagation.ts
@@ -1,5 +1,8 @@
-export const stopPropagation = (ev: JSX.TargetedMouseEvent<HTMLDivElement>, _consume?: any) => {
-    ev.preventDefault();
-    ev.stopPropagation();
-    return true;
+export const stopPropagation = (
+	ev: JSX.TargetedMouseEvent<HTMLDivElement>,
+	_consume?: any,
+) => {
+	ev.preventDefault();
+	ev.stopPropagation();
+	return true;
 };
diff --git a/src/lib/vortex/Signaling.ts b/src/lib/vortex/Signaling.ts
index b54a6110174688a3b1956f01c019776115b1a598..5def9042b186c5e26fb6aaf15737f09fe1b47514 100644
--- a/src/lib/vortex/Signaling.ts
+++ b/src/lib/vortex/Signaling.ts
@@ -1,188 +1,189 @@
 import EventEmitter from "eventemitter3";
+
 import {
-    RtpCapabilities,
-    RtpParameters
+	RtpCapabilities,
+	RtpParameters,
 } from "mediasoup-client/lib/RtpParameters";
 import { DtlsParameters } from "mediasoup-client/lib/Transport";
 
 import {
-    AuthenticationResult,
-    Room,
-    TransportInitDataTuple,
-    WSCommandType,
-    WSErrorCode,
-    ProduceType,
-    ConsumerData
+	AuthenticationResult,
+	Room,
+	TransportInitDataTuple,
+	WSCommandType,
+	WSErrorCode,
+	ProduceType,
+	ConsumerData,
 } from "./Types";
 
 interface SignalingEvents {
-    open: (event: Event) => void;
-    close: (event: CloseEvent) => void;
-    error: (event: Event) => void;
-    data: (data: any) => void;
+	open: (event: Event) => void;
+	close: (event: CloseEvent) => void;
+	error: (event: Event) => void;
+	data: (data: any) => void;
 }
 
 export default class Signaling extends EventEmitter<SignalingEvents> {
-    ws?: WebSocket;
-    index: number;
-    pending: Map<number, (data: unknown) => void>;
-
-    constructor() {
-        super();
-        this.index = 0;
-        this.pending = new Map();
-    }
-
-    connected(): boolean {
-        return (
-            this.ws !== undefined &&
-            this.ws.readyState !== WebSocket.CLOSING &&
-            this.ws.readyState !== WebSocket.CLOSED
-        );
-    }
-
-    connect(address: string): Promise<void> {
-        this.disconnect();
-        this.ws = new WebSocket(address);
-        this.ws.onopen = e => this.emit("open", e);
-        this.ws.onclose = e => this.emit("close", e);
-        this.ws.onerror = e => this.emit("error", e);
-        this.ws.onmessage = e => this.parseData(e);
-
-        let finished = false;
-        return new Promise((resolve, reject) => {
-            this.once("open", () => {
-                if (finished) return;
-                finished = true;
-                resolve();
-            });
-
-            this.once("error", () => {
-                if (finished) return;
-                finished = true;
-                reject();
-            });
-        });
-    }
-
-    disconnect() {
-        if (
-            this.ws !== undefined &&
-            this.ws.readyState !== WebSocket.CLOSED &&
-            this.ws.readyState !== WebSocket.CLOSING
-        )
-            this.ws.close(1000);
-    }
-
-    private parseData(event: MessageEvent) {
-        if (typeof event.data !== "string") return;
-        const json = JSON.parse(event.data);
-        const entry = this.pending.get(json.id);
-        if (entry === undefined) {
-            this.emit("data", json);
-            return;
-        }
-
-        entry(json);
-    }
-
-    sendRequest(type: string, data?: any): Promise<any> {
-        if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
-            return Promise.reject({ error: WSErrorCode.NotConnected });
-
-        const ws = this.ws;
-        return new Promise((resolve, reject) => {
-            if (this.index >= 2 ** 32) this.index = 0;
-            while (this.pending.has(this.index)) this.index++;
-            const onClose = (e: CloseEvent) => {
-                reject({
-                    error: e.code,
-                    message: e.reason
-                });
-            };
-
-            const finishedFn = (data: any) => {
-                this.removeListener("close", onClose);
-                if (data.error)
-                    reject({
-                        error: data.error,
-                        message: data.message,
-                        data: data.data
-                    });
-                resolve(data.data);
-            };
-
-            this.pending.set(this.index, finishedFn);
-            this.once("close", onClose);
-            const json = {
-                id: this.index,
-                type: type,
-                data
-            };
-            ws.send(JSON.stringify(json) + "\n");
-            this.index++;
-        });
-    }
-
-    authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
-        return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
-    }
-
-    async roomInfo(): Promise<Room> {
-        const room = await this.sendRequest(WSCommandType.RoomInfo);
-        return {
-            id: room.id,
-            videoAllowed: room.videoAllowed,
-            users: new Map(Object.entries(room.users))
-        };
-    }
-
-    initializeTransports(
-        rtpCapabilities: RtpCapabilities
-    ): Promise<TransportInitDataTuple> {
-        return this.sendRequest(WSCommandType.InitializeTransports, {
-            mode: "SplitWebRTC",
-            rtpCapabilities
-        });
-    }
-
-    connectTransport(
-        id: string,
-        dtlsParameters: DtlsParameters
-    ): Promise<void> {
-        return this.sendRequest(WSCommandType.ConnectTransport, {
-            id,
-            dtlsParameters
-        });
-    }
-
-    async startProduce(
-        type: ProduceType,
-        rtpParameters: RtpParameters
-    ): Promise<string> {
-        let result = await this.sendRequest(WSCommandType.StartProduce, {
-            type,
-            rtpParameters
-        });
-        return result.producerId;
-    }
-
-    stopProduce(type: ProduceType): Promise<void> {
-        return this.sendRequest(WSCommandType.StopProduce, { type });
-    }
-
-    startConsume(userId: string, type: ProduceType): Promise<ConsumerData> {
-        return this.sendRequest(WSCommandType.StartConsume, { type, userId });
-    }
-
-    stopConsume(consumerId: string): Promise<void> {
-        return this.sendRequest(WSCommandType.StopConsume, { id: consumerId });
-    }
-
-    setConsumerPause(consumerId: string, paused: boolean): Promise<void> {
-        return this.sendRequest(WSCommandType.SetConsumerPause, {
-            id: consumerId,
-            paused
-        });
-    }
+	ws?: WebSocket;
+	index: number;
+	pending: Map<number, (data: unknown) => void>;
+
+	constructor() {
+		super();
+		this.index = 0;
+		this.pending = new Map();
+	}
+
+	connected(): boolean {
+		return (
+			this.ws !== undefined &&
+			this.ws.readyState !== WebSocket.CLOSING &&
+			this.ws.readyState !== WebSocket.CLOSED
+		);
+	}
+
+	connect(address: string): Promise<void> {
+		this.disconnect();
+		this.ws = new WebSocket(address);
+		this.ws.onopen = (e) => this.emit("open", e);
+		this.ws.onclose = (e) => this.emit("close", e);
+		this.ws.onerror = (e) => this.emit("error", e);
+		this.ws.onmessage = (e) => this.parseData(e);
+
+		let finished = false;
+		return new Promise((resolve, reject) => {
+			this.once("open", () => {
+				if (finished) return;
+				finished = true;
+				resolve();
+			});
+
+			this.once("error", () => {
+				if (finished) return;
+				finished = true;
+				reject();
+			});
+		});
+	}
+
+	disconnect() {
+		if (
+			this.ws !== undefined &&
+			this.ws.readyState !== WebSocket.CLOSED &&
+			this.ws.readyState !== WebSocket.CLOSING
+		)
+			this.ws.close(1000);
+	}
+
+	private parseData(event: MessageEvent) {
+		if (typeof event.data !== "string") return;
+		const json = JSON.parse(event.data);
+		const entry = this.pending.get(json.id);
+		if (entry === undefined) {
+			this.emit("data", json);
+			return;
+		}
+
+		entry(json);
+	}
+
+	sendRequest(type: string, data?: any): Promise<any> {
+		if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
+			return Promise.reject({ error: WSErrorCode.NotConnected });
+
+		const ws = this.ws;
+		return new Promise((resolve, reject) => {
+			if (this.index >= 2 ** 32) this.index = 0;
+			while (this.pending.has(this.index)) this.index++;
+			const onClose = (e: CloseEvent) => {
+				reject({
+					error: e.code,
+					message: e.reason,
+				});
+			};
+
+			const finishedFn = (data: any) => {
+				this.removeListener("close", onClose);
+				if (data.error)
+					reject({
+						error: data.error,
+						message: data.message,
+						data: data.data,
+					});
+				resolve(data.data);
+			};
+
+			this.pending.set(this.index, finishedFn);
+			this.once("close", onClose);
+			const json = {
+				id: this.index,
+				type: type,
+				data,
+			};
+			ws.send(JSON.stringify(json) + "\n");
+			this.index++;
+		});
+	}
+
+	authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
+		return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
+	}
+
+	async roomInfo(): Promise<Room> {
+		const room = await this.sendRequest(WSCommandType.RoomInfo);
+		return {
+			id: room.id,
+			videoAllowed: room.videoAllowed,
+			users: new Map(Object.entries(room.users)),
+		};
+	}
+
+	initializeTransports(
+		rtpCapabilities: RtpCapabilities,
+	): Promise<TransportInitDataTuple> {
+		return this.sendRequest(WSCommandType.InitializeTransports, {
+			mode: "SplitWebRTC",
+			rtpCapabilities,
+		});
+	}
+
+	connectTransport(
+		id: string,
+		dtlsParameters: DtlsParameters,
+	): Promise<void> {
+		return this.sendRequest(WSCommandType.ConnectTransport, {
+			id,
+			dtlsParameters,
+		});
+	}
+
+	async startProduce(
+		type: ProduceType,
+		rtpParameters: RtpParameters,
+	): Promise<string> {
+		let result = await this.sendRequest(WSCommandType.StartProduce, {
+			type,
+			rtpParameters,
+		});
+		return result.producerId;
+	}
+
+	stopProduce(type: ProduceType): Promise<void> {
+		return this.sendRequest(WSCommandType.StopProduce, { type });
+	}
+
+	startConsume(userId: string, type: ProduceType): Promise<ConsumerData> {
+		return this.sendRequest(WSCommandType.StartConsume, { type, userId });
+	}
+
+	stopConsume(consumerId: string): Promise<void> {
+		return this.sendRequest(WSCommandType.StopConsume, { id: consumerId });
+	}
+
+	setConsumerPause(consumerId: string, paused: boolean): Promise<void> {
+		return this.sendRequest(WSCommandType.SetConsumerPause, {
+			id: consumerId,
+			paused,
+		});
+	}
 }
diff --git a/src/lib/vortex/Types.ts b/src/lib/vortex/Types.ts
index ceaf4e912f4acb45efca74a371b7646e68c7593a..5ab09a43e94ecbe4f5ef16bc0706c2b19086fe99 100644
--- a/src/lib/vortex/Types.ts
+++ b/src/lib/vortex/Types.ts
@@ -1,111 +1,111 @@
 import { Consumer } from "mediasoup-client/lib/Consumer";
 import {
-    MediaKind,
-    RtpCapabilities,
-    RtpParameters
+	MediaKind,
+	RtpCapabilities,
+	RtpParameters,
 } from "mediasoup-client/lib/RtpParameters";
 import { SctpParameters } from "mediasoup-client/lib/SctpParameters";
 import {
-    DtlsParameters,
-    IceCandidate,
-    IceParameters
+	DtlsParameters,
+	IceCandidate,
+	IceParameters,
 } from "mediasoup-client/lib/Transport";
 
 export enum WSEventType {
-    UserJoined = "UserJoined",
-    UserLeft = "UserLeft",
+	UserJoined = "UserJoined",
+	UserLeft = "UserLeft",
 
-    UserStartProduce = "UserStartProduce",
-    UserStopProduce = "UserStopProduce"
+	UserStartProduce = "UserStartProduce",
+	UserStopProduce = "UserStopProduce",
 }
 
 export enum WSCommandType {
-    Authenticate = "Authenticate",
-    RoomInfo = "RoomInfo",
+	Authenticate = "Authenticate",
+	RoomInfo = "RoomInfo",
 
-    InitializeTransports = "InitializeTransports",
-    ConnectTransport = "ConnectTransport",
+	InitializeTransports = "InitializeTransports",
+	ConnectTransport = "ConnectTransport",
 
-    StartProduce = "StartProduce",
-    StopProduce = "StopProduce",
+	StartProduce = "StartProduce",
+	StopProduce = "StopProduce",
 
-    StartConsume = "StartConsume",
-    StopConsume = "StopConsume",
-    SetConsumerPause = "SetConsumerPause"
+	StartConsume = "StartConsume",
+	StopConsume = "StopConsume",
+	SetConsumerPause = "SetConsumerPause",
 }
 
 export enum WSErrorCode {
-    NotConnected = 0,
-    NotFound = 404,
+	NotConnected = 0,
+	NotFound = 404,
 
-    TransportConnectionFailure = 601,
+	TransportConnectionFailure = 601,
 
-    ProducerFailure = 611,
-    ProducerNotFound = 614,
+	ProducerFailure = 611,
+	ProducerNotFound = 614,
 
-    ConsumerFailure = 621,
-    ConsumerNotFound = 624
+	ConsumerFailure = 621,
+	ConsumerNotFound = 624,
 }
 
 export enum WSCloseCode {
-    // Sent when the received data is not a string, or is unparseable
-    InvalidData = 1003,
-    Unauthorized = 4001,
-    RoomClosed = 4004,
-    // Sent when a client tries to send an opcode in the wrong state
-    InvalidState = 1002,
-    ServerError = 1011
+	// Sent when the received data is not a string, or is unparseable
+	InvalidData = 1003,
+	Unauthorized = 4001,
+	RoomClosed = 4004,
+	// Sent when a client tries to send an opcode in the wrong state
+	InvalidState = 1002,
+	ServerError = 1011,
 }
 
 export interface VoiceError {
-    error: WSErrorCode | WSCloseCode;
-    message: string;
+	error: WSErrorCode | WSCloseCode;
+	message: string;
 }
 
 export type ProduceType = "audio"; //| "video" | "saudio" | "svideo";
 
 export interface AuthenticationResult {
-    userId: string;
-    roomId: string;
-    rtpCapabilities: RtpCapabilities;
+	userId: string;
+	roomId: string;
+	rtpCapabilities: RtpCapabilities;
 }
 
 export interface Room {
-    id: string;
-    videoAllowed: boolean;
-    users: Map<string, VoiceUser>;
+	id: string;
+	videoAllowed: boolean;
+	users: Map<string, VoiceUser>;
 }
 
 export interface VoiceUser {
-    audio?: boolean;
-    //video?: boolean,
-    //saudio?: boolean,
-    //svideo?: boolean,
+	audio?: boolean;
+	//video?: boolean,
+	//saudio?: boolean,
+	//svideo?: boolean,
 }
 
 export interface ConsumerList {
-    audio?: Consumer;
-    //video?: Consumer,
-    //saudio?: Consumer,
-    //svideo?: Consumer,
+	audio?: Consumer;
+	//video?: Consumer,
+	//saudio?: Consumer,
+	//svideo?: Consumer,
 }
 
 export interface TransportInitData {
-    id: string;
-    iceParameters: IceParameters;
-    iceCandidates: IceCandidate[];
-    dtlsParameters: DtlsParameters;
-    sctpParameters: SctpParameters | undefined;
+	id: string;
+	iceParameters: IceParameters;
+	iceCandidates: IceCandidate[];
+	dtlsParameters: DtlsParameters;
+	sctpParameters: SctpParameters | undefined;
 }
 
 export interface TransportInitDataTuple {
-    sendTransport: TransportInitData;
-    recvTransport: TransportInitData;
+	sendTransport: TransportInitData;
+	recvTransport: TransportInitData;
 }
 
 export interface ConsumerData {
-    id: string;
-    producerId: string;
-    kind: MediaKind;
-    rtpParameters: RtpParameters;
+	id: string;
+	producerId: string;
+	kind: MediaKind;
+	rtpParameters: RtpParameters;
 }
diff --git a/src/lib/vortex/VoiceClient.ts b/src/lib/vortex/VoiceClient.ts
index 903295bb3d44f8ef83ade8e6a27e55879c53ab3e..9ebeab60ea7ecb8fdc642426d7b7810f01e17551 100644
--- a/src/lib/vortex/VoiceClient.ts
+++ b/src/lib/vortex/VoiceClient.ts
@@ -1,331 +1,331 @@
 import EventEmitter from "eventemitter3";
-
 import * as mediasoupClient from "mediasoup-client";
+
 import {
-    Device,
-    Producer,
-    Transport,
-    UnsupportedError
+	Device,
+	Producer,
+	Transport,
+	UnsupportedError,
 } from "mediasoup-client/lib/types";
 
+import Signaling from "./Signaling";
 import {
-    ProduceType,
-    WSEventType,
-    VoiceError,
-    VoiceUser,
-    ConsumerList,
-    WSErrorCode
+	ProduceType,
+	WSEventType,
+	VoiceError,
+	VoiceUser,
+	ConsumerList,
+	WSErrorCode,
 } from "./Types";
-import Signaling from "./Signaling";
 
 interface VoiceEvents {
-    ready: () => void;
-    error: (error: Error) => void;
-    close: (error?: VoiceError) => void;
+	ready: () => void;
+	error: (error: Error) => void;
+	close: (error?: VoiceError) => void;
 
-    startProduce: (type: ProduceType) => void;
-    stopProduce: (type: ProduceType) => void;
+	startProduce: (type: ProduceType) => void;
+	stopProduce: (type: ProduceType) => void;
 
-    userJoined: (userId: string) => void;
-    userLeft: (userId: string) => void;
+	userJoined: (userId: string) => void;
+	userLeft: (userId: string) => void;
 
-    userStartProduce: (userId: string, type: ProduceType) => void;
-    userStopProduce: (userId: string, type: ProduceType) => void;
+	userStartProduce: (userId: string, type: ProduceType) => void;
+	userStopProduce: (userId: string, type: ProduceType) => void;
 }
 
 export default class VoiceClient extends EventEmitter<VoiceEvents> {
-    private _supported: boolean;
-
-    device?: Device;
-    signaling: Signaling;
-
-    sendTransport?: Transport;
-    recvTransport?: Transport;
-
-    userId?: string;
-    roomId?: string;
-    participants: Map<string, VoiceUser>;
-    consumers: Map<string, ConsumerList>;
-
-    audioProducer?: Producer;
-    constructor() {
-        super();
-        this._supported = mediasoupClient.detectDevice() !== undefined;
-        this.signaling = new Signaling();
-
-        this.participants = new Map();
-        this.consumers = new Map();
-
-        this.signaling.on(
-            "data",
-            json => {
-                const data = json.data;
-                switch (json.type) {
-                    case WSEventType.UserJoined: {
-                        this.participants.set(data.id, {});
-                        this.emit("userJoined", data.id);
-                        break;
-                    }
-                    case WSEventType.UserLeft: {
-                        this.participants.delete(data.id);
-                        this.emit("userLeft", data.id);
-
-                        if (this.recvTransport) this.stopConsume(data.id);
-                        break;
-                    }
-                    case WSEventType.UserStartProduce: {
-                        const user = this.participants.get(data.id);
-                        if (user === undefined) return;
-                        switch (data.type) {
-                            case "audio":
-                                user.audio = true;
-                                break;
-                            default:
-                                throw new Error(
-                                    `Invalid produce type ${data.type}`
-                                );
-                        }
-
-                        if (this.recvTransport)
-                            this.startConsume(data.id, data.type);
-                        this.emit("userStartProduce", data.id, data.type);
-                        break;
-                    }
-                    case WSEventType.UserStopProduce: {
-                        const user = this.participants.get(data.id);
-                        if (user === undefined) return;
-                        switch (data.type) {
-                            case "audio":
-                                user.audio = false;
-                                break;
-                            default:
-                                throw new Error(
-                                    `Invalid produce type ${data.type}`
-                                );
-                        }
-
-                        if (this.recvTransport)
-                            this.stopConsume(data.id, data.type);
-                        this.emit("userStopProduce", data.id, data.type);
-                        break;
-                    }
-                }
-            },
-            this
-        );
-
-        this.signaling.on(
-            "error",
-            error => {
-                this.emit("error", new Error("Signaling error"));
-            },
-            this
-        );
-
-        this.signaling.on(
-            "close",
-            error => {
-                this.disconnect(
-                    {
-                        error: error.code,
-                        message: error.reason
-                    },
-                    true
-                );
-            },
-            this
-        );
-    }
-
-    supported() {
-        return this._supported;
-    }
-    throwIfUnsupported() {
-        if (!this._supported) throw new UnsupportedError("RTC not supported");
-    }
-
-    connect(address: string, roomId: string) {
-        this.throwIfUnsupported();
-        this.device = new Device();
-        this.roomId = roomId;
-        return this.signaling.connect(address);
-    }
-
-    disconnect(error?: VoiceError, ignoreDisconnected?: boolean) {
-        if (!this.signaling.connected() && !ignoreDisconnected) return;
-        this.signaling.disconnect();
-        this.participants = new Map();
-        this.consumers = new Map();
-        this.userId = undefined;
-        this.roomId = undefined;
-
-        this.audioProducer = undefined;
-
-        if (this.sendTransport) this.sendTransport.close();
-        if (this.recvTransport) this.recvTransport.close();
-        this.sendTransport = undefined;
-        this.recvTransport = undefined;
-
-        this.emit("close", error);
-    }
-
-    async authenticate(token: string) {
-        this.throwIfUnsupported();
-        if (this.device === undefined || this.roomId === undefined)
-            throw new ReferenceError("Voice Client is in an invalid state");
-        const result = await this.signaling.authenticate(token, this.roomId);
-        let [room] = await Promise.all([
-            this.signaling.roomInfo(),
-            this.device.load({ routerRtpCapabilities: result.rtpCapabilities })
-        ]);
-
-        this.userId = result.userId;
-        this.participants = room.users;
-    }
-
-    async initializeTransports() {
-        this.throwIfUnsupported();
-        if (this.device === undefined)
-            throw new ReferenceError("Voice Client is in an invalid state");
-        const initData = await this.signaling.initializeTransports(
-            this.device.rtpCapabilities
-        );
-
-        this.sendTransport = this.device.createSendTransport(
-            initData.sendTransport
-        );
-        this.recvTransport = this.device.createRecvTransport(
-            initData.recvTransport
-        );
-
-        const connectTransport = (transport: Transport) => {
-            transport.on("connect", ({ dtlsParameters }, callback, errback) => {
-                this.signaling
-                    .connectTransport(transport.id, dtlsParameters)
-                    .then(callback)
-                    .catch(errback);
-            });
-        };
-
-        connectTransport(this.sendTransport);
-        connectTransport(this.recvTransport);
-
-        this.sendTransport.on("produce", (parameters, callback, errback) => {
-            const type = parameters.appData.type;
-            if (
-                parameters.kind === "audio" &&
-                type !== "audio" &&
-                type !== "saudio"
-            )
-                return errback();
-            if (
-                parameters.kind === "video" &&
-                type !== "video" &&
-                type !== "svideo"
-            )
-                return errback();
-            this.signaling
-                .startProduce(type, parameters.rtpParameters)
-                .then(id => callback({ id }))
-                .catch(errback);
-        });
-
-        this.emit("ready");
-        for (let user of this.participants) {
-            if (user[1].audio && user[0] !== this.userId)
-                this.startConsume(user[0], "audio");
-        }
-    }
-
-    private async startConsume(userId: string, type: ProduceType) {
-        if (this.recvTransport === undefined)
-            throw new Error("Receive transport undefined");
-        const consumers = this.consumers.get(userId) || {};
-        const consumerParams = await this.signaling.startConsume(userId, type);
-        const consumer = await this.recvTransport.consume(consumerParams);
-        switch (type) {
-            case "audio":
-                consumers.audio = consumer;
-        }
-
-        const mediaStream = new MediaStream([consumer.track]);
-        const audio = new Audio();
-        audio.srcObject = mediaStream;
-        await this.signaling.setConsumerPause(consumer.id, false);
-        audio.play();
-        this.consumers.set(userId, consumers);
-    }
-
-    private async stopConsume(userId: string, type?: ProduceType) {
-        const consumers = this.consumers.get(userId);
-        if (consumers === undefined) return;
-        if (type === undefined) {
-            if (consumers.audio !== undefined) consumers.audio.close();
-            this.consumers.delete(userId);
-        } else {
-            switch (type) {
-                case "audio": {
-                    if (consumers.audio !== undefined) {
-                        consumers.audio.close();
-                        this.signaling.stopConsume(consumers.audio.id);
-                    }
-                    consumers.audio = undefined;
-                    break;
-                }
-            }
-
-            this.consumers.set(userId, consumers);
-        }
-    }
-
-    async startProduce(track: MediaStreamTrack, type: ProduceType) {
-        if (this.sendTransport === undefined)
-            throw new Error("Send transport undefined");
-        const producer = await this.sendTransport.produce({
-            track,
-            appData: { type }
-        });
-
-        switch (type) {
-            case "audio":
-                this.audioProducer = producer;
-                break;
-        }
-
-        const participant = this.participants.get(this.userId || "");
-        if (participant !== undefined) {
-            participant[type] = true;
-            this.participants.set(this.userId || "", participant);
-        }
-
-        this.emit("startProduce", type);
-    }
-
-    async stopProduce(type: ProduceType) {
-        let producer;
-        switch (type) {
-            case "audio":
-                producer = this.audioProducer;
-                this.audioProducer = undefined;
-                break;
-        }
-
-        if (producer !== undefined) {
-            producer.close();
-            this.emit("stopProduce", type);
-        }
-
-        const participant = this.participants.get(this.userId || "");
-        if (participant !== undefined) {
-            participant[type] = false;
-            this.participants.set(this.userId || "", participant);
-        }
-
-        try {
-            await this.signaling.stopProduce(type);
-        } catch (error) {
-            if (error.error === WSErrorCode.ProducerNotFound) return;
-            else throw error;
-        }
-    }
+	private _supported: boolean;
+
+	device?: Device;
+	signaling: Signaling;
+
+	sendTransport?: Transport;
+	recvTransport?: Transport;
+
+	userId?: string;
+	roomId?: string;
+	participants: Map<string, VoiceUser>;
+	consumers: Map<string, ConsumerList>;
+
+	audioProducer?: Producer;
+	constructor() {
+		super();
+		this._supported = mediasoupClient.detectDevice() !== undefined;
+		this.signaling = new Signaling();
+
+		this.participants = new Map();
+		this.consumers = new Map();
+
+		this.signaling.on(
+			"data",
+			(json) => {
+				const data = json.data;
+				switch (json.type) {
+					case WSEventType.UserJoined: {
+						this.participants.set(data.id, {});
+						this.emit("userJoined", data.id);
+						break;
+					}
+					case WSEventType.UserLeft: {
+						this.participants.delete(data.id);
+						this.emit("userLeft", data.id);
+
+						if (this.recvTransport) this.stopConsume(data.id);
+						break;
+					}
+					case WSEventType.UserStartProduce: {
+						const user = this.participants.get(data.id);
+						if (user === undefined) return;
+						switch (data.type) {
+							case "audio":
+								user.audio = true;
+								break;
+							default:
+								throw new Error(
+									`Invalid produce type ${data.type}`,
+								);
+						}
+
+						if (this.recvTransport)
+							this.startConsume(data.id, data.type);
+						this.emit("userStartProduce", data.id, data.type);
+						break;
+					}
+					case WSEventType.UserStopProduce: {
+						const user = this.participants.get(data.id);
+						if (user === undefined) return;
+						switch (data.type) {
+							case "audio":
+								user.audio = false;
+								break;
+							default:
+								throw new Error(
+									`Invalid produce type ${data.type}`,
+								);
+						}
+
+						if (this.recvTransport)
+							this.stopConsume(data.id, data.type);
+						this.emit("userStopProduce", data.id, data.type);
+						break;
+					}
+				}
+			},
+			this,
+		);
+
+		this.signaling.on(
+			"error",
+			(error) => {
+				this.emit("error", new Error("Signaling error"));
+			},
+			this,
+		);
+
+		this.signaling.on(
+			"close",
+			(error) => {
+				this.disconnect(
+					{
+						error: error.code,
+						message: error.reason,
+					},
+					true,
+				);
+			},
+			this,
+		);
+	}
+
+	supported() {
+		return this._supported;
+	}
+	throwIfUnsupported() {
+		if (!this._supported) throw new UnsupportedError("RTC not supported");
+	}
+
+	connect(address: string, roomId: string) {
+		this.throwIfUnsupported();
+		this.device = new Device();
+		this.roomId = roomId;
+		return this.signaling.connect(address);
+	}
+
+	disconnect(error?: VoiceError, ignoreDisconnected?: boolean) {
+		if (!this.signaling.connected() && !ignoreDisconnected) return;
+		this.signaling.disconnect();
+		this.participants = new Map();
+		this.consumers = new Map();
+		this.userId = undefined;
+		this.roomId = undefined;
+
+		this.audioProducer = undefined;
+
+		if (this.sendTransport) this.sendTransport.close();
+		if (this.recvTransport) this.recvTransport.close();
+		this.sendTransport = undefined;
+		this.recvTransport = undefined;
+
+		this.emit("close", error);
+	}
+
+	async authenticate(token: string) {
+		this.throwIfUnsupported();
+		if (this.device === undefined || this.roomId === undefined)
+			throw new ReferenceError("Voice Client is in an invalid state");
+		const result = await this.signaling.authenticate(token, this.roomId);
+		let [room] = await Promise.all([
+			this.signaling.roomInfo(),
+			this.device.load({ routerRtpCapabilities: result.rtpCapabilities }),
+		]);
+
+		this.userId = result.userId;
+		this.participants = room.users;
+	}
+
+	async initializeTransports() {
+		this.throwIfUnsupported();
+		if (this.device === undefined)
+			throw new ReferenceError("Voice Client is in an invalid state");
+		const initData = await this.signaling.initializeTransports(
+			this.device.rtpCapabilities,
+		);
+
+		this.sendTransport = this.device.createSendTransport(
+			initData.sendTransport,
+		);
+		this.recvTransport = this.device.createRecvTransport(
+			initData.recvTransport,
+		);
+
+		const connectTransport = (transport: Transport) => {
+			transport.on("connect", ({ dtlsParameters }, callback, errback) => {
+				this.signaling
+					.connectTransport(transport.id, dtlsParameters)
+					.then(callback)
+					.catch(errback);
+			});
+		};
+
+		connectTransport(this.sendTransport);
+		connectTransport(this.recvTransport);
+
+		this.sendTransport.on("produce", (parameters, callback, errback) => {
+			const type = parameters.appData.type;
+			if (
+				parameters.kind === "audio" &&
+				type !== "audio" &&
+				type !== "saudio"
+			)
+				return errback();
+			if (
+				parameters.kind === "video" &&
+				type !== "video" &&
+				type !== "svideo"
+			)
+				return errback();
+			this.signaling
+				.startProduce(type, parameters.rtpParameters)
+				.then((id) => callback({ id }))
+				.catch(errback);
+		});
+
+		this.emit("ready");
+		for (let user of this.participants) {
+			if (user[1].audio && user[0] !== this.userId)
+				this.startConsume(user[0], "audio");
+		}
+	}
+
+	private async startConsume(userId: string, type: ProduceType) {
+		if (this.recvTransport === undefined)
+			throw new Error("Receive transport undefined");
+		const consumers = this.consumers.get(userId) || {};
+		const consumerParams = await this.signaling.startConsume(userId, type);
+		const consumer = await this.recvTransport.consume(consumerParams);
+		switch (type) {
+			case "audio":
+				consumers.audio = consumer;
+		}
+
+		const mediaStream = new MediaStream([consumer.track]);
+		const audio = new Audio();
+		audio.srcObject = mediaStream;
+		await this.signaling.setConsumerPause(consumer.id, false);
+		audio.play();
+		this.consumers.set(userId, consumers);
+	}
+
+	private async stopConsume(userId: string, type?: ProduceType) {
+		const consumers = this.consumers.get(userId);
+		if (consumers === undefined) return;
+		if (type === undefined) {
+			if (consumers.audio !== undefined) consumers.audio.close();
+			this.consumers.delete(userId);
+		} else {
+			switch (type) {
+				case "audio": {
+					if (consumers.audio !== undefined) {
+						consumers.audio.close();
+						this.signaling.stopConsume(consumers.audio.id);
+					}
+					consumers.audio = undefined;
+					break;
+				}
+			}
+
+			this.consumers.set(userId, consumers);
+		}
+	}
+
+	async startProduce(track: MediaStreamTrack, type: ProduceType) {
+		if (this.sendTransport === undefined)
+			throw new Error("Send transport undefined");
+		const producer = await this.sendTransport.produce({
+			track,
+			appData: { type },
+		});
+
+		switch (type) {
+			case "audio":
+				this.audioProducer = producer;
+				break;
+		}
+
+		const participant = this.participants.get(this.userId || "");
+		if (participant !== undefined) {
+			participant[type] = true;
+			this.participants.set(this.userId || "", participant);
+		}
+
+		this.emit("startProduce", type);
+	}
+
+	async stopProduce(type: ProduceType) {
+		let producer;
+		switch (type) {
+			case "audio":
+				producer = this.audioProducer;
+				this.audioProducer = undefined;
+				break;
+		}
+
+		if (producer !== undefined) {
+			producer.close();
+			this.emit("stopProduce", type);
+		}
+
+		const participant = this.participants.get(this.userId || "");
+		if (participant !== undefined) {
+			participant[type] = false;
+			this.participants.set(this.userId || "", participant);
+		}
+
+		try {
+			await this.signaling.stopProduce(type);
+		} catch (error) {
+			if (error.error === WSErrorCode.ProducerNotFound) return;
+			else throw error;
+		}
+	}
 }
diff --git a/src/lib/windowSize.ts b/src/lib/windowSize.ts
index 55089b4e103ad3eda85ce0d2bec1493340b09382..770dcd6cfa64ad5da677383f5bbace0c6bcd50d4 100644
--- a/src/lib/windowSize.ts
+++ b/src/lib/windowSize.ts
@@ -1,24 +1,24 @@
 import { useEffect, useState } from "preact/hooks";
 
 export function useWindowSize() {
-    const [windowSize, setWindowSize] = useState({
-        width: window.innerWidth,
-        height: window.innerHeight
-    });
+	const [windowSize, setWindowSize] = useState({
+		width: window.innerWidth,
+		height: window.innerHeight,
+	});
 
-    useEffect(() => {
-        function handleResize() {
-            setWindowSize({
-                width: window.innerWidth,
-                height: window.innerHeight
-            });
-        }
+	useEffect(() => {
+		function handleResize() {
+			setWindowSize({
+				width: window.innerWidth,
+				height: window.innerHeight,
+			});
+		}
 
-        window.addEventListener("resize", handleResize);
-        handleResize();
+		window.addEventListener("resize", handleResize);
+		handleResize();
 
-        return () => window.removeEventListener("resize", handleResize);
-    }, []);
+		return () => window.removeEventListener("resize", handleResize);
+	}, []);
 
-    return windowSize;
+	return windowSize;
 }
diff --git a/src/main.tsx b/src/main.tsx
index 02c4364bb37502b066c0f0c4e8ebd3ac1d32b330..c842979523e9a97c0ee9f61aa631cc49f5a245d3 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,19 +1,21 @@
-import { registerSW } from 'virtual:pwa-register';
-import { internalEmit } from './lib/eventEmitter';
-
-export const updateSW = registerSW({
-    onNeedRefresh() {
-        internalEmit('PWA', 'update');
-    },
-    onOfflineReady() {
-        console.info('Ready to work offline.');
-        // show a ready to work offline to user
-    },
-})
+import { registerSW } from "virtual:pwa-register";
 
 import "./styles/index.scss";
 import { render } from "preact";
+
+import { internalEmit } from "./lib/eventEmitter";
+
 import { App } from "./pages/app";
 
+export const updateSW = registerSW({
+	onNeedRefresh() {
+		internalEmit("PWA", "update");
+	},
+	onOfflineReady() {
+		console.info("Ready to work offline.");
+		// show a ready to work offline to user
+	},
+});
+
 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 render(<App />, document.getElementById("app")!);
diff --git a/src/pages/Open.tsx b/src/pages/Open.tsx
index 7ee8120cfee956098ff60e9606faf0f4c9216f03..32a471c31b496a56fdee50247630e3d2d19ac06a 100644
--- a/src/pages/Open.tsx
+++ b/src/pages/Open.tsx
@@ -1,72 +1,83 @@
+import { useHistory, useParams } from "react-router-dom";
+
 import { Text } from "preact-i18n";
-import Header from "../components/ui/Header";
 import { useContext, useEffect } from "preact/hooks";
-import { useHistory, useParams } from "react-router-dom";
+
 import { useIntermediate } from "../context/intermediate/Intermediate";
-import { useChannels, useForceUpdate, useUser } from "../context/revoltjs/hooks";
-import { AppContext, ClientStatus, StatusContext } from "../context/revoltjs/RevoltClient";
+import {
+	AppContext,
+	ClientStatus,
+	StatusContext,
+} from "../context/revoltjs/RevoltClient";
+import {
+	useChannels,
+	useForceUpdate,
+	useUser,
+} from "../context/revoltjs/hooks";
+
+import Header from "../components/ui/Header";
 
 export default function Open() {
-    const history = useHistory();
-    const client = useContext(AppContext);
-    const status = useContext(StatusContext);
-    const { id } = useParams<{ id: string }>();
-    const { openScreen } = useIntermediate();
+	const history = useHistory();
+	const client = useContext(AppContext);
+	const status = useContext(StatusContext);
+	const { id } = useParams<{ id: string }>();
+	const { openScreen } = useIntermediate();
 
-    if (status !== ClientStatus.ONLINE) {
-        return (
-            <Header placement="primary">
-                <Text id="general.loading" />
-            </Header>
-        );
-    }
+	if (status !== ClientStatus.ONLINE) {
+		return (
+			<Header placement="primary">
+				<Text id="general.loading" />
+			</Header>
+		);
+	}
 
-    const ctx = useForceUpdate();
-    const channels = useChannels(undefined, ctx);
-    const user = useUser(id, ctx);
+	const ctx = useForceUpdate();
+	const channels = useChannels(undefined, ctx);
+	const user = useUser(id, ctx);
 
-    useEffect(() => {
-        if (id === "saved") {
-            for (const channel of channels) {
-                if (channel?.channel_type === "SavedMessages") {
-                    history.push(`/channel/${channel._id}`);
-                    return;
-                }
-            }
+	useEffect(() => {
+		if (id === "saved") {
+			for (const channel of channels) {
+				if (channel?.channel_type === "SavedMessages") {
+					history.push(`/channel/${channel._id}`);
+					return;
+				}
+			}
 
-            client.users
-                .openDM(client.user?._id as string)
-                .then(channel => history.push(`/channel/${channel?._id}`))
-                .catch(error => openScreen({ id: "error", error }));
+			client.users
+				.openDM(client.user?._id as string)
+				.then((channel) => history.push(`/channel/${channel?._id}`))
+				.catch((error) => openScreen({ id: "error", error }));
 
-            return;
-        }
+			return;
+		}
 
-        if (user) {
-            const channel: string | undefined = channels.find(
-                channel =>
-                    channel?.channel_type === "DirectMessage" &&
-                    channel.recipients.includes(id)
-            )?._id;
+		if (user) {
+			const channel: string | undefined = channels.find(
+				(channel) =>
+					channel?.channel_type === "DirectMessage" &&
+					channel.recipients.includes(id),
+			)?._id;
 
-            if (channel) {
-                history.push(`/channel/${channel}`);
-            } else {
-                client.users
-                    .openDM(id)
-                    .then(channel => history.push(`/channel/${channel?._id}`))
-                    .catch(error => openScreen({ id: "error", error }));
-            }
+			if (channel) {
+				history.push(`/channel/${channel}`);
+			} else {
+				client.users
+					.openDM(id)
+					.then((channel) => history.push(`/channel/${channel?._id}`))
+					.catch((error) => openScreen({ id: "error", error }));
+			}
 
-            return;
-        }
+			return;
+		}
 
-        history.push("/");
-    }, []);
+		history.push("/");
+	}, []);
 
-    return (
-        <Header placement="primary">
-            <Text id="general.loading" />
-        </Header>
-    );
+	return (
+		<Header placement="primary">
+			<Text id="general.loading" />
+		</Header>
+	);
 }
diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx
index 9b8cbcd66e9e8ba04684ec9ae3766ffec83e8556..da941deef896279370fd3fc8d7603a7b99eea3ff 100644
--- a/src/pages/RevoltApp.tsx
+++ b/src/pages/RevoltApp.tsx
@@ -1,84 +1,120 @@
 import { Docked, OverlappingPanels, ShowIf } from "react-overlapping-panels";
-import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
 import { Switch, Route, useLocation } from "react-router-dom";
 import styled from "styled-components";
 
 import ContextMenus from "../lib/ContextMenus";
+import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
+
 import Popovers from "../context/intermediate/Popovers";
-import SyncManager from "../context/revoltjs/SyncManager";
-import StateMonitor from "../context/revoltjs/StateMonitor";
 import Notifications from "../context/revoltjs/Notifications";
+import StateMonitor from "../context/revoltjs/StateMonitor";
+import SyncManager from "../context/revoltjs/SyncManager";
 
+import BottomNavigation from "../components/navigation/BottomNavigation";
 import LeftSidebar from "../components/navigation/LeftSidebar";
 import RightSidebar from "../components/navigation/RightSidebar";
-import BottomNavigation from "../components/navigation/BottomNavigation";
-
 import Open from "./Open";
-import Home from './home/Home';
-import Invite from "./invite/Invite";
-import Friends from "./friends/Friends";
 import Channel from "./channels/Channel";
-import Settings from './settings/Settings';
 import Developer from "./developer/Developer";
-import ServerSettings from "./settings/ServerSettings";
+import Friends from "./friends/Friends";
+import Home from "./home/Home";
+import Invite from "./invite/Invite";
 import ChannelSettings from "./settings/ChannelSettings";
+import ServerSettings from "./settings/ServerSettings";
+import Settings from "./settings/Settings";
 
 const Routes = styled.div`
-    min-width: 0;
-    display: flex;
-    overflow: hidden;
-    flex-direction: column;
-    background: var(--primary-background);
+	min-width: 0;
+	display: flex;
+	overflow: hidden;
+	flex-direction: column;
+	background: var(--primary-background);
 `;
 
 export default function App() {
-    const path = useLocation().pathname;
-    const fixedBottomNav = (path === '/' || path === '/settings' || path.startsWith("/friends"));
-    const inSettings = path.includes('/settings');
-    const inChannel = path.includes('/channel');
-    const inSpecial = (path.startsWith("/friends") && isTouchscreenDevice) || path.startsWith('/invite') || path.startsWith("/settings");
+	const path = useLocation().pathname;
+	const fixedBottomNav =
+		path === "/" || path === "/settings" || path.startsWith("/friends");
+	const inSettings = path.includes("/settings");
+	const inChannel = path.includes("/channel");
+	const inSpecial =
+		(path.startsWith("/friends") && isTouchscreenDevice) ||
+		path.startsWith("/invite") ||
+		path.startsWith("/settings");
+
+	return (
+		<OverlappingPanels
+			width="100vw"
+			height="var(--app-height)"
+			leftPanel={
+				inSpecial
+					? undefined
+					: { width: 292, component: <LeftSidebar /> }
+			}
+			rightPanel={
+				!inSettings && inChannel
+					? { width: 240, component: <RightSidebar /> }
+					: undefined
+			}
+			bottomNav={{
+				component: <BottomNavigation />,
+				showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
+				height: 50,
+			}}
+			docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
+			<Routes>
+				<Switch>
+					<Route
+						path="/server/:server/channel/:channel/settings/:page"
+						component={ChannelSettings}
+					/>
+					<Route
+						path="/server/:server/channel/:channel/settings"
+						component={ChannelSettings}
+					/>
+					<Route
+						path="/server/:server/settings/:page"
+						component={ServerSettings}
+					/>
+					<Route
+						path="/server/:server/settings"
+						component={ServerSettings}
+					/>
+					<Route
+						path="/channel/:channel/settings/:page"
+						component={ChannelSettings}
+					/>
+					<Route
+						path="/channel/:channel/settings"
+						component={ChannelSettings}
+					/>
 
-    return (
-        <OverlappingPanels
-            width="100vw"
-            height="var(--app-height)"
-            leftPanel={inSpecial ? undefined : { width: 292, component: <LeftSidebar /> }}
-            rightPanel={(!inSettings && inChannel) ? { width: 240, component: <RightSidebar /> } : undefined}
-            bottomNav={{
-                component: <BottomNavigation />,
-                showIf: fixedBottomNav ? ShowIf.Always : ShowIf.Left,
-                height: 50
-            }}
-            docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
-            <Routes>
-                <Switch>
-                    <Route path="/server/:server/channel/:channel/settings/:page" component={ChannelSettings} />
-                    <Route path="/server/:server/channel/:channel/settings" component={ChannelSettings} />
-                    <Route path="/server/:server/settings/:page" component={ServerSettings} />
-                    <Route path="/server/:server/settings" component={ServerSettings} />
-                    <Route path="/channel/:channel/settings/:page" component={ChannelSettings} />
-                    <Route path="/channel/:channel/settings" component={ChannelSettings} />
+					<Route
+						path="/channel/:channel/message/:message"
+						component={Channel}
+					/>
+					<Route
+						path="/server/:server/channel/:channel"
+						component={Channel}
+					/>
+					<Route path="/server/:server" />
+					<Route path="/channel/:channel" component={Channel} />
 
-                    <Route path="/channel/:channel/message/:message" component={Channel} />
-                    <Route path="/server/:server/channel/:channel" component={Channel} />
-                    <Route path="/server/:server" />
-                    <Route path="/channel/:channel" component={Channel} />
-                    
-                    <Route path="/settings/:page" component={Settings} />
-                    <Route path="/settings" component={Settings} />
+					<Route path="/settings/:page" component={Settings} />
+					<Route path="/settings" component={Settings} />
 
-                    <Route path="/dev" component={Developer} />
-                    <Route path="/friends" component={Friends} />
-                    <Route path="/open/:id" component={Open} />
-                    <Route path="/invite/:code" component={Invite} />
-                    <Route path="/" component={Home} />
-                </Switch>
-            </Routes>
-            <ContextMenus />
-            <Popovers />
-            <Notifications />
-            <StateMonitor />
-            <SyncManager />
-        </OverlappingPanels>
-    );
-};
+					<Route path="/dev" component={Developer} />
+					<Route path="/friends" component={Friends} />
+					<Route path="/open/:id" component={Open} />
+					<Route path="/invite/:code" component={Invite} />
+					<Route path="/" component={Home} />
+				</Switch>
+			</Routes>
+			<ContextMenus />
+			<Popovers />
+			<Notifications />
+			<StateMonitor />
+			<SyncManager />
+		</OverlappingPanels>
+	);
+}
diff --git a/src/pages/app.tsx b/src/pages/app.tsx
index 9e1d0482a63e61713db16fca28711b234b21575d..d5a23a47d42446df9f33c3a0f123c8627a02dea4 100644
--- a/src/pages/app.tsx
+++ b/src/pages/app.tsx
@@ -1,33 +1,36 @@
-import { CheckAuth } from "../context/revoltjs/CheckAuth";
-import Preloader from "../components/ui/Preloader";
 import { Route, Switch } from "react-router-dom";
-import Masks from "../components/ui/Masks";
-import Context from "../context";
 
 import { lazy, Suspense } from "preact/compat";
-const Login = lazy(() => import('./login/Login'));
-const RevoltApp = lazy(() => import('./RevoltApp'));
+
+import Context from "../context";
+import { CheckAuth } from "../context/revoltjs/CheckAuth";
+
+import Masks from "../components/ui/Masks";
+import Preloader from "../components/ui/Preloader";
+
+const Login = lazy(() => import("./login/Login"));
+const RevoltApp = lazy(() => import("./RevoltApp"));
 
 export function App() {
-    return (
-        <Context>
-            <Masks />
-            {/* 
+	return (
+		<Context>
+			<Masks />
+			{/* 
             // @ts-expect-error */}
-            <Suspense fallback={<Preloader type="spinner" />}>
-                <Switch>
-                    <Route path="/login">
-                        <CheckAuth>
-                            <Login />
-                        </CheckAuth>
-                    </Route>
-                    <Route path="/">
-                        <CheckAuth auth>
-                            <RevoltApp />
-                        </CheckAuth>
-                    </Route>
-                </Switch>
-            </Suspense>
-        </Context>
-    );
+			<Suspense fallback={<Preloader type="spinner" />}>
+				<Switch>
+					<Route path="/login">
+						<CheckAuth>
+							<Login />
+						</CheckAuth>
+					</Route>
+					<Route path="/">
+						<CheckAuth auth>
+							<RevoltApp />
+						</CheckAuth>
+					</Route>
+				</Switch>
+			</Suspense>
+		</Context>
+	);
 }
diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx
index 6daf10963c17bdab23256931a3100d8d36e0601a..0b4c9129e8f8dfb645d14fc30bff527d65ba3a2d 100644
--- a/src/pages/channels/Channel.tsx
+++ b/src/pages/channels/Channel.tsx
@@ -1,122 +1,153 @@
-import styled from "styled-components";
-import { useEffect, useState } from "preact/hooks";
-import ChannelHeader from "./ChannelHeader";
 import { useParams, useHistory } from "react-router-dom";
-import { MessageArea } from "./messaging/MessageArea";
-import Checkbox from "../../components/ui/Checkbox";
-import Button from "../../components/ui/Button";
-// import { useRenderState } from "../../lib/renderer/Singleton";
+import { Channels } from "revolt.js/dist/api/objects";
+import styled from "styled-components";
+
+import { useState } from "preact/hooks";
+
 import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
-import MessageBox from "../../components/common/messaging/MessageBox";
+
 import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
-import MemberSidebar from "../../components/navigation/right/MemberSidebar";
+
+import MessageBox from "../../components/common/messaging/MessageBox";
 import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
 import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
-import { Channel } from "revolt.js";
+import Button from "../../components/ui/Button";
+import Checkbox from "../../components/ui/Checkbox";
+
+import MemberSidebar from "../../components/navigation/right/MemberSidebar";
+import ChannelHeader from "./ChannelHeader";
+import { MessageArea } from "./messaging/MessageArea";
 import VoiceHeader from "./voice/VoiceHeader";
 
 const ChannelMain = styled.div`
-    flex-grow: 1;
-    display: flex;
-    min-height: 0;
-    overflow: hidden;
-    flex-direction: row;
+	flex-grow: 1;
+	display: flex;
+	min-height: 0;
+	overflow: hidden;
+	flex-direction: row;
 `;
 
 const ChannelContent = styled.div`
-    flex-grow: 1;
-    display: flex;
-    overflow: hidden;
-    flex-direction: column;
+	flex-grow: 1;
+	display: flex;
+	overflow: hidden;
+	flex-direction: column;
 `;
 
 const AgeGate = styled.div`
-    display: flex;
-    flex-grow: 1;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    user-select: none;
-    padding: 12px;
-
-    img {
-        height: 150px;
-    }
-
-    .subtext {
-        color: var(--secondary-foreground);
-        margin-bottom: 12px;
-        font-size: 14px;
-    }
-
-    .actions {
-        margin-top: 20px;
-        display: flex;
-        gap: 12px;
-    }
+	display: flex;
+	flex-grow: 1;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	user-select: none;
+	padding: 12px;
+
+	img {
+		height: 150px;
+	}
+
+	.subtext {
+		color: var(--secondary-foreground);
+		margin-bottom: 12px;
+		font-size: 14px;
+	}
+
+	.actions {
+		margin-top: 20px;
+		display: flex;
+		gap: 12px;
+	}
 `;
 
 export function Channel({ id }: { id: string }) {
-    const ctx = useForceUpdate();
-    const channel = useChannel(id, ctx);
+	const ctx = useForceUpdate();
+	const channel = useChannel(id, ctx);
 
-    if (!channel) return null;
+	if (!channel) return null;
 
-    if (channel.channel_type === 'VoiceChannel') {
-        return <VoiceChannel channel={channel} />;
-    } else {
-        return <TextChannel channel={channel} />;
-    }
+	if (channel.channel_type === "VoiceChannel") {
+		return <VoiceChannel channel={channel} />;
+	} else {
+		return <TextChannel channel={channel} />;
+	}
 }
 
-function TextChannel({ channel }: { channel: Channel }) {
-    const [ showMembers, setMembers ] = useState(true);
-
-    if ((channel.channel_type === 'TextChannel' || channel.channel_type === 'Group') && channel.name.includes('nsfw')) {
-        const goBack = useHistory();
-        const [ consent, setConsent ] = useState(false);
-        const [ ageGate, setAgeGate ] = useState(false);
-        if (!ageGate) {
-            return (
-                <AgeGate>
-                    <img src={"https://static.revolt.chat/emoji/mutant/26a0.svg"} draggable={false}/>
-                    <h2>{channel.name}</h2>
-                    <span className="subtext">This channel is marked as NSFW. <a href="#">Learn more</a></span>
-
-                    <Checkbox checked={consent} onChange={v => setConsent(v)}>I confirm that I am at least 18 years old.</Checkbox>
-                    <div className="actions">
-                        <Button contrast onClick={() => goBack}>Go back</Button>
-                        <Button contrast onClick={() => consent && setAgeGate(true)}>Enter Channel</Button>
-                    </div>
-                </AgeGate>
-            )
-        }
-    }
-
-    let id = channel._id;
-    return <>
-        <ChannelHeader channel={channel} toggleSidebar={() => setMembers(!showMembers)} />
-        <ChannelMain>
-            <ChannelContent>
-                <VoiceHeader id={id} />
-                <MessageArea id={id} />
-                <TypingIndicator id={id} />
-                <JumpToBottom id={id} />
-                <MessageBox channel={channel} />
-            </ChannelContent>
-            { !isTouchscreenDevice && showMembers && <MemberSidebar channel={channel} /> }
-        </ChannelMain>
-    </>;
+function TextChannel({ channel }: { channel: Channels.Channel }) {
+	const [showMembers, setMembers] = useState(true);
+
+	if (
+		(channel.channel_type === "TextChannel" ||
+			channel.channel_type === "Group") &&
+		channel.name.includes("nsfw")
+	) {
+		const goBack = useHistory();
+		const [consent, setConsent] = useState(false);
+		const [ageGate, setAgeGate] = useState(false);
+		if (!ageGate) {
+			return (
+				<AgeGate>
+					<img
+						src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
+						draggable={false}
+					/>
+					<h2>{channel.name}</h2>
+					<span className="subtext">
+						This channel is marked as NSFW.{" "}
+						<a href="#">Learn more</a>
+					</span>
+
+					<Checkbox checked={consent} onChange={(v) => setConsent(v)}>
+						I confirm that I am at least 18 years old.
+					</Checkbox>
+					<div className="actions">
+						<Button contrast onClick={() => goBack}>
+							Go back
+						</Button>
+						<Button
+							contrast
+							onClick={() => consent && setAgeGate(true)}>
+							Enter Channel
+						</Button>
+					</div>
+				</AgeGate>
+			);
+		}
+	}
+
+	let id = channel._id;
+	return (
+		<>
+			<ChannelHeader
+				channel={channel}
+				toggleSidebar={() => setMembers(!showMembers)}
+			/>
+			<ChannelMain>
+				<ChannelContent>
+					<VoiceHeader id={id} />
+					<MessageArea id={id} />
+					<TypingIndicator id={id} />
+					<JumpToBottom id={id} />
+					<MessageBox channel={channel} />
+				</ChannelContent>
+				{!isTouchscreenDevice && showMembers && (
+					<MemberSidebar channel={channel} />
+				)}
+			</ChannelMain>
+		</>
+	);
 }
 
-function VoiceChannel({ channel }: { channel: Channel }) {
-    return <>
-        <ChannelHeader channel={channel} />
-        <VoiceHeader id={channel._id} />
-    </>;
+function VoiceChannel({ channel }: { channel: Channels.Channel }) {
+	return (
+		<>
+			<ChannelHeader channel={channel} />
+			<VoiceHeader id={channel._id} />
+		</>
+	);
 }
 
-export default function() {
-    const { channel } = useParams<{ channel: string }>();
-    return <Channel id={channel} key={channel} />;
+export default function () {
+	const { channel } = useParams<{ channel: string }>();
+	return <Channel id={channel} key={channel} />;
 }
diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx
index 5dae6a04de8ada269e3c29d64b700a4d27aa6fbc..7a0736948c6dd3ee19cbda85a354ed25863abfe0 100644
--- a/src/pages/channels/ChannelHeader.tsx
+++ b/src/pages/channels/ChannelHeader.tsx
@@ -1,116 +1,139 @@
-import styled from "styled-components";
+import { At, Hash } from "@styled-icons/boxicons-regular";
+import { Notepad, Group } from "@styled-icons/boxicons-solid";
 import { Channel, User } from "revolt.js";
+import styled from "styled-components";
+
 import { useContext } from "preact/hooks";
-import Header from "../../components/ui/Header";
-import HeaderActions from "./actions/HeaderActions";
-import Markdown from "../../components/markdown/Markdown";
-import { getChannelName } from "../../context/revoltjs/util";
-import UserStatus from "../../components/common/user/UserStatus";
+
+import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+
+import { useIntermediate } from "../../context/intermediate/Intermediate";
 import { AppContext } from "../../context/revoltjs/RevoltClient";
-import { At, Hash } from "@styled-icons/boxicons-regular";
-import { Notepad, Group } from "@styled-icons/boxicons-solid";
+import { getChannelName } from "../../context/revoltjs/util";
+
 import { useStatusColour } from "../../components/common/user/UserIcon";
-import { useIntermediate } from "../../context/intermediate/Intermediate";
-import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+import UserStatus from "../../components/common/user/UserStatus";
+import Header from "../../components/ui/Header";
+
+import Markdown from "../../components/markdown/Markdown";
+import HeaderActions from "./actions/HeaderActions";
 
 export interface ChannelHeaderProps {
-    channel: Channel,
-    toggleSidebar?: () => void
+	channel: Channel;
+	toggleSidebar?: () => void;
 }
 
 const Info = styled.div`
-    flex-grow: 1;
-    min-width: 0;
-    overflow: hidden;
-    white-space: nowrap;
+	flex-grow: 1;
+	min-width: 0;
+	overflow: hidden;
+	white-space: nowrap;
 
-    display: flex;
-    gap: 8px;
-    align-items: center;
+	display: flex;
+	gap: 8px;
+	align-items: center;
 
-    * {
-        display: inline-block;
-    }
+	* {
+		display: inline-block;
+	}
 
-    .divider {
-        height: 20px;
-        margin: 0 5px;
-        padding-left: 1px;
-        background-color: var(--tertiary-background);
-    }
+	.divider {
+		height: 20px;
+		margin: 0 5px;
+		padding-left: 1px;
+		background-color: var(--tertiary-background);
+	}
 
-    .status {
-        width: 10px;
-        height: 10px;
-        border-radius: 50%;
-        display: inline-block;
-        margin-inline-end: 6px;
-    }
+	.status {
+		width: 10px;
+		height: 10px;
+		border-radius: 50%;
+		display: inline-block;
+		margin-inline-end: 6px;
+	}
 
-    .desc {
-        cursor: pointer;
-        margin-top: 2px;
-        font-size: 0.8em;
-        font-weight: 400;
-        color: var(--secondary-foreground);
-    }
+	.desc {
+		cursor: pointer;
+		margin-top: 2px;
+		font-size: 0.8em;
+		font-weight: 400;
+		color: var(--secondary-foreground);
+	}
 `;
 
-export default function ChannelHeader({ channel, toggleSidebar }: ChannelHeaderProps) {
-    const { openScreen } = useIntermediate();
-    const client = useContext(AppContext);
+export default function ChannelHeader({
+	channel,
+	toggleSidebar,
+}: ChannelHeaderProps) {
+	const { openScreen } = useIntermediate();
+	const client = useContext(AppContext);
 
-    const name = getChannelName(client, channel);
-    let icon, recipient;
-    switch (channel.channel_type) {
-        case "SavedMessages":
-            icon = <Notepad size={24} />;
-            break;
-        case "DirectMessage":
-            icon = <At size={24} />;
-            const uid = client.channels.getRecipient(channel._id);
-            recipient = client.users.get(uid);
-            break;
-        case "Group":
-            icon = <Group size={24} />;
-            break;
-        case "TextChannel":
-            icon = <Hash size={24} />;
-            break;
-    }
+	const name = getChannelName(client, channel);
+	let icon, recipient;
+	switch (channel.channel_type) {
+		case "SavedMessages":
+			icon = <Notepad size={24} />;
+			break;
+		case "DirectMessage":
+			icon = <At size={24} />;
+			const uid = client.channels.getRecipient(channel._id);
+			recipient = client.users.get(uid);
+			break;
+		case "Group":
+			icon = <Group size={24} />;
+			break;
+		case "TextChannel":
+			icon = <Hash size={24} />;
+			break;
+	}
 
-    return (
-        <Header placement="primary">
-            { icon }
-            <Info>
-                <span className="name">{ name }</span>
-                {isTouchscreenDevice && channel.channel_type === "DirectMessage" && (
-                    <>
-                        <div className="divider" />
-                        <span className="desc">
-                            <div className="status" style={{ backgroundColor: useStatusColour(recipient as User) }} />
-                            <UserStatus user={recipient as User} />
-                        </span>
-                    </>
-                )}
-                {!isTouchscreenDevice && (channel.channel_type === "Group" || channel.channel_type === "TextChannel") && channel.description && (
-                    <>
-                        <div className="divider" />
-                        <span
-                            className="desc"
-                            onClick={() =>
-                                openScreen({
-                                    id: "channel_info",
-                                    channel_id: channel._id
-                                })
-                            }>
-                                
-                            <Markdown content={channel.description.split("\n")[0] ?? ""} disallowBigEmoji />
-                        </span>
-                    </>
-                )}
-            </Info>
-            <HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
-        </Header>
-    )
+	return (
+		<Header placement="primary">
+			{icon}
+			<Info>
+				<span className="name">{name}</span>
+				{isTouchscreenDevice &&
+					channel.channel_type === "DirectMessage" && (
+						<>
+							<div className="divider" />
+							<span className="desc">
+								<div
+									className="status"
+									style={{
+										backgroundColor: useStatusColour(
+											recipient as User,
+										),
+									}}
+								/>
+								<UserStatus user={recipient as User} />
+							</span>
+						</>
+					)}
+				{!isTouchscreenDevice &&
+					(channel.channel_type === "Group" ||
+						channel.channel_type === "TextChannel") &&
+					channel.description && (
+						<>
+							<div className="divider" />
+							<span
+								className="desc"
+								onClick={() =>
+									openScreen({
+										id: "channel_info",
+										channel_id: channel._id,
+									})
+								}>
+								<Markdown
+									content={
+										channel.description.split("\n")[0] ?? ""
+									}
+									disallowBigEmoji
+								/>
+							</span>
+						</>
+					)}
+			</Info>
+			<HeaderActions channel={channel} toggleSidebar={toggleSidebar} />
+		</Header>
+	);
 }
diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx
index 881ea0dd3492c3abca81b2710bdda12e43295fd6..096835db5088bf0757035fb523a7c38d97aa31ee 100644
--- a/src/pages/channels/actions/HeaderActions.tsx
+++ b/src/pages/channels/actions/HeaderActions.tsx
@@ -1,81 +1,112 @@
-import { useContext } from "preact/hooks";
+import { Sidebar as SidebarIcon } from "@styled-icons/boxicons-regular";
+import {
+	UserPlus,
+	Cog,
+	PhoneCall,
+	PhoneOutgoing,
+} from "@styled-icons/boxicons-solid";
 import { useHistory } from "react-router-dom";
-import { ChannelHeaderProps } from "../ChannelHeader";
-import IconButton from "../../../components/ui/IconButton";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import { useContext } from "preact/hooks";
+
 import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
-import UpdateIndicator from "../../../components/common/UpdateIndicator";
+
+import {
+	VoiceContext,
+	VoiceOperationsContext,
+	VoiceStatus,
+} from "../../../context/Voice";
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { VoiceContext, VoiceOperationsContext, VoiceStatus } from "../../../context/Voice";
-import { UserPlus, Cog, PhoneCall, PhoneOutgoing } from "@styled-icons/boxicons-solid";
-import { Sidebar as SidebarIcon } from "@styled-icons/boxicons-regular";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import UpdateIndicator from "../../../components/common/UpdateIndicator";
+import IconButton from "../../../components/ui/IconButton";
+
+import { ChannelHeaderProps } from "../ChannelHeader";
 
-export default function HeaderActions({ channel, toggleSidebar }: ChannelHeaderProps) {
-    const { openScreen } = useIntermediate();
-    const client = useContext(AppContext);
-    const history = useHistory();
+export default function HeaderActions({
+	channel,
+	toggleSidebar,
+}: ChannelHeaderProps) {
+	const { openScreen } = useIntermediate();
+	const client = useContext(AppContext);
+	const history = useHistory();
 
-    return (
-        <>
-            <UpdateIndicator />
-            { channel.channel_type === "Group" && (
-                <>
-                    <IconButton onClick={() =>
-                        openScreen({
-                            id: "user_picker",
-                            omit: channel.recipients,
-                            callback: async users => {
-                                for (const user of users) {
-                                    await client.channels.addMember(channel._id, user);
-                                }
-                            }
-                        })}>
-                        <UserPlus size={27} />
-                    </IconButton>
-                    <IconButton onClick={() => history.push(`/channel/${channel._id}/settings`)}>
-                        <Cog size={24} />
-                    </IconButton>
-                </>
-            ) }
-            <VoiceActions channel={channel} />
-            { (channel.channel_type === "Group" || channel.channel_type === "TextChannel") && !isTouchscreenDevice && (
-                <IconButton onClick={toggleSidebar}>
-                    <SidebarIcon size={22} />
-                </IconButton>
-            ) }
-        </>
-    )
+	return (
+		<>
+			<UpdateIndicator />
+			{channel.channel_type === "Group" && (
+				<>
+					<IconButton
+						onClick={() =>
+							openScreen({
+								id: "user_picker",
+								omit: channel.recipients,
+								callback: async (users) => {
+									for (const user of users) {
+										await client.channels.addMember(
+											channel._id,
+											user,
+										);
+									}
+								},
+							})
+						}>
+						<UserPlus size={27} />
+					</IconButton>
+					<IconButton
+						onClick={() =>
+							history.push(`/channel/${channel._id}/settings`)
+						}>
+						<Cog size={24} />
+					</IconButton>
+				</>
+			)}
+			<VoiceActions channel={channel} />
+			{(channel.channel_type === "Group" ||
+				channel.channel_type === "TextChannel") &&
+				!isTouchscreenDevice && (
+					<IconButton onClick={toggleSidebar}>
+						<SidebarIcon size={22} />
+					</IconButton>
+				)}
+		</>
+	);
 }
 
-function VoiceActions({ channel }: Pick<ChannelHeaderProps, 'channel'>) {
-    if (channel.channel_type === 'SavedMessages' ||
-        channel.channel_type === 'TextChannel') return null;
+function VoiceActions({ channel }: Pick<ChannelHeaderProps, "channel">) {
+	if (
+		channel.channel_type === "SavedMessages" ||
+		channel.channel_type === "TextChannel"
+	)
+		return null;
 
-    const voice = useContext(VoiceContext);
-    const { connect, disconnect } = useContext(VoiceOperationsContext);
+	const voice = useContext(VoiceContext);
+	const { connect, disconnect } = useContext(VoiceOperationsContext);
 
-    if (voice.status >= VoiceStatus.READY) {
-        if (voice.roomId === channel._id) {
-            return (
-                <IconButton onClick={disconnect}>
-                    <PhoneOutgoing size={22} />
-                </IconButton>
-            )
-        } else {
-            return (
-                <IconButton onClick={() => {
-                    disconnect();
-                    connect(channel._id);
-                }}>
-                    <PhoneCall size={24} />
-                </IconButton>
-            )
-        }
-    } else {
-        return (
-            <IconButton>
-                <PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
-            </IconButton>
-        )
-    }
+	if (voice.status >= VoiceStatus.READY) {
+		if (voice.roomId === channel._id) {
+			return (
+				<IconButton onClick={disconnect}>
+					<PhoneOutgoing size={22} />
+				</IconButton>
+			);
+		} else {
+			return (
+				<IconButton
+					onClick={() => {
+						disconnect();
+						connect(channel._id);
+					}}>
+					<PhoneCall size={24} />
+				</IconButton>
+			);
+		}
+	} else {
+		return (
+			<IconButton>
+				<PhoneCall size={24} /** ! FIXME: TEMP */ color="red" />
+			</IconButton>
+		);
+	}
 }
diff --git a/src/pages/channels/messaging/ConversationStart.tsx b/src/pages/channels/messaging/ConversationStart.tsx
index 21ecadb12b4372707919d2414f781536ebfd2c7f..9fe82d7004a787d7fd8edafee083716b462e0be2 100644
--- a/src/pages/channels/messaging/ConversationStart.tsx
+++ b/src/pages/channels/messaging/ConversationStart.tsx
@@ -1,38 +1,40 @@
-import { Text } from "preact-i18n";
 import styled from "styled-components";
-import { getChannelName } from "../../../context/revoltjs/util";
+
+import { Text } from "preact-i18n";
+
 import { useChannel, useForceUpdate } from "../../../context/revoltjs/hooks";
+import { getChannelName } from "../../../context/revoltjs/util";
 
 const StartBase = styled.div`
-    margin: 18px 16px 10px 16px;
-
-    h1 {
-        font-size: 23px;
-        margin: 0 0 8px 0;
-    }
-
-    h4 {
-        font-weight: 400;
-        margin: 0;
-        font-size: 14px;
-    }
+	margin: 18px 16px 10px 16px;
+
+	h1 {
+		font-size: 23px;
+		margin: 0 0 8px 0;
+	}
+
+	h4 {
+		font-weight: 400;
+		margin: 0;
+		font-size: 14px;
+	}
 `;
 
 interface Props {
-    id: string;
+	id: string;
 }
 
 export default function ConversationStart({ id }: Props) {
-    const ctx = useForceUpdate();
-    const channel = useChannel(id, ctx);
-    if (!channel) return null;
-
-    return (
-        <StartBase>
-            <h1>{ getChannelName(ctx.client, channel, true) }</h1>
-            <h4>
-                <Text id="app.main.channel.start.group" />
-            </h4>
-        </StartBase>
-    );
+	const ctx = useForceUpdate();
+	const channel = useChannel(id, ctx);
+	if (!channel) return null;
+
+	return (
+		<StartBase>
+			<h1>{getChannelName(ctx.client, channel, true)}</h1>
+			<h4>
+				<Text id="app.main.channel.start.group" />
+			</h4>
+		</StartBase>
+	);
 }
diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx
index f69ae67800c077a5baad8e2c1b257a9b291af418..7fe720d15191a24857a637c586c3e939287b1326 100644
--- a/src/pages/channels/messaging/MessageArea.tsx
+++ b/src/pages/channels/messaging/MessageArea.tsx
@@ -1,232 +1,259 @@
-import styled from "styled-components";
-import { createContext } from "preact";
 import { animateScroll } from "react-scroll";
-import MessageRenderer from "./MessageRenderer";
-import ConversationStart from './ConversationStart';
+import styled from "styled-components";
 import useResizeObserver from "use-resize-observer";
-import Preloader from "../../../components/ui/Preloader";
-import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
-import { RenderState, ScrollState } from "../../../lib/renderer/types";
-import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
-import { IntermediateContext } from "../../../context/intermediate/Intermediate";
-import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
-import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
+
+import { createContext } from "preact";
+import {
+	useContext,
+	useEffect,
+	useLayoutEffect,
+	useRef,
+	useState,
+} from "preact/hooks";
+
 import { defer } from "../../../lib/defer";
 import { internalEmit } from "../../../lib/eventEmitter";
+import { SingletonMessageRenderer } from "../../../lib/renderer/Singleton";
+import { RenderState, ScrollState } from "../../../lib/renderer/types";
+
+import { IntermediateContext } from "../../../context/intermediate/Intermediate";
+import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
+import {
+	ClientStatus,
+	StatusContext,
+} from "../../../context/revoltjs/RevoltClient";
+
+import Preloader from "../../../components/ui/Preloader";
+
+import ConversationStart from "./ConversationStart";
+import MessageRenderer from "./MessageRenderer";
 
 const Area = styled.div`
-    height: 100%;
-    flex-grow: 1;
-    min-height: 0;
-    overflow-x: hidden;
-    overflow-y: scroll;
-    word-break: break-word;
-
-    > div {
-        display: flex;
-        min-height: 100%;
-        padding-bottom: 20px;
-        flex-direction: column;
-        justify-content: flex-end;
-    }
+	height: 100%;
+	flex-grow: 1;
+	min-height: 0;
+	overflow-x: hidden;
+	overflow-y: scroll;
+	word-break: break-word;
+
+	> div {
+		display: flex;
+		min-height: 100%;
+		padding-bottom: 20px;
+		flex-direction: column;
+		justify-content: flex-end;
+	}
 `;
 
 interface Props {
-    id: string;
+	id: string;
 }
 
 export const MessageAreaWidthContext = createContext(0);
 export const MESSAGE_AREA_PADDING = 82;
 
 export function MessageArea({ id }: Props) {
-    const status = useContext(StatusContext);
-    const { focusTaken } = useContext(IntermediateContext);
-
-    // ? This is the scroll container.
-    const ref = useRef<HTMLDivElement>(null);
-    const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
-
-    // ? Current channel state.
-    const [state, setState] = useState<RenderState>({ type: "LOADING" });
-
-    // ? useRef to avoid re-renders
-    const scrollState = useRef<ScrollState>({ type: "Free" });
-
-    const setScrollState = (v: ScrollState) => {
-        if (v.type === 'StayAtBottom') {
-            if (scrollState.current.type === 'Bottom' || atBottom()) {
-                scrollState.current = { type: 'ScrollToBottom', smooth: v.smooth };
-            } else {
-                scrollState.current = { type: 'Free' };
-            }
-        } else {
-            scrollState.current = v;
-        }
-
-        defer(() => {
-            if (scrollState.current.type === "ScrollToBottom") {
-                setScrollState({ type: "Bottom", scrollingUntil: + new Date() + 150 });
-                
-                animateScroll.scrollToBottom({
-                    container: ref.current,
-                    duration: scrollState.current.smooth ? 150 : 0
-                });
-            } else if (scrollState.current.type === "OffsetTop") {
-                animateScroll.scrollTo(
-                    Math.max(
-                        101,
-                        ref.current.scrollTop +
-                            (ref.current.scrollHeight - scrollState.current.previousHeight)
-                    ),
-                    {
-                        container: ref.current,
-                        duration: 0
-                    }
-                );
-
-                setScrollState({ type: "Free" });
-            } else if (scrollState.current.type === "ScrollTop") {
-                animateScroll.scrollTo(scrollState.current.y, {
-                    container: ref.current,
-                    duration: 0
-                });
-
-                setScrollState({ type: "Free" });
-            }
-        });
-    }
-
-    // ? Determine if we are at the bottom of the scroll container.
-    // -> https://stackoverflow.com/a/44893438
-    // By default, we assume we are at the bottom, i.e. when we first load.
-    const atBottom = (offset = 0) =>
-        ref.current
-            ? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
-                  offset <=
-              ref.current.clientHeight
-            : true;
-    
-    const atTop = (offset = 0) => ref.current.scrollTop <= offset;
-
-    // ? Handle events from renderer.
-    useEffect(() => {
-        SingletonMessageRenderer.addListener('state', setState);
-        return () => SingletonMessageRenderer.removeListener('state', setState);
-    }, [ ]);
-
-    useEffect(() => {
-        SingletonMessageRenderer.addListener('scroll', setScrollState);
-        return () => SingletonMessageRenderer.removeListener('scroll', setScrollState);
-    }, [ scrollState ]);
-
-    // ? Load channel initially.
-    useEffect(() => {
-        SingletonMessageRenderer.init(id);
-    }, [ id ]);
-
-    // ? If we are waiting for network, try again.
-    useEffect(() => {
-        switch (status) {
-            case ClientStatus.ONLINE:
-                if (state.type === 'WAITING_FOR_NETWORK') {
-                    SingletonMessageRenderer.init(id);
-                } else {
-                    SingletonMessageRenderer.reloadStale(id);
-                }
-
-                break;
-            case ClientStatus.OFFLINE:
-            case ClientStatus.DISCONNECTED:
-            case ClientStatus.CONNECTING:
-                SingletonMessageRenderer.markStale();
-                break;
-        }
-    }, [ status, state ]);
-
-    // ? When the container is scrolled.
-    // ? Also handle StayAtBottom
-    useEffect(() => {
-        async function onScroll() {
-            if (scrollState.current.type === "Free" && atBottom()) {
-                setScrollState({ type: "Bottom" });
-            } else if (scrollState.current.type === "Bottom" && !atBottom()) {
-                if (scrollState.current.scrollingUntil && scrollState.current.scrollingUntil > + new Date()) return;
-                setScrollState({ type: "Free" });
-            }
-        }
-
-        ref.current.addEventListener("scroll", onScroll);
-        return () => ref.current.removeEventListener("scroll", onScroll);
-    }, [ref, scrollState]);
-
-    // ? Top and bottom loaders.
-    useEffect(() => {
-        async function onScroll() {
-            if (atTop(100)) {
-                SingletonMessageRenderer.loadTop(ref.current);
-            }
-
-            if (atBottom(100)) {
-                SingletonMessageRenderer.loadBottom(ref.current);
-            }
-        }
-
-        ref.current.addEventListener("scroll", onScroll);
-        return () => ref.current.removeEventListener("scroll", onScroll);
-    }, [ref]);
-
-    // ? Scroll down whenever the message area resizes.
-    function stbOnResize() {
-        if (!atBottom() && scrollState.current.type === "Bottom") {
-            animateScroll.scrollToBottom({
-                container: ref.current,
-                duration: 0
-            });
-
-            setScrollState({ type: "Bottom" });
-        }
-    }
-
-    // ? Scroll down when container resized.
-    useLayoutEffect(() => {
-        stbOnResize();
-    }, [height]);
-
-    // ? Scroll down whenever the window resizes.
-    useLayoutEffect(() => {
-        document.addEventListener("resize", stbOnResize);
-        return () => document.removeEventListener("resize", stbOnResize);
-    }, [ref, scrollState]);
-
-    // ? Scroll to bottom when pressing 'Escape'.
-    useEffect(() => {
-        function keyUp(e: KeyboardEvent) {
-            if (e.key === "Escape" && !focusTaken) {
-                SingletonMessageRenderer.jumpToBottom(id, true);
-                internalEmit("TextArea", "focus", "message");
-            }
-        }
-
-        document.body.addEventListener("keyup", keyUp);
-        return () => document.body.removeEventListener("keyup", keyUp);
-    }, [ref, focusTaken]);
-
-    return (
-        <MessageAreaWidthContext.Provider value={(width ?? 0) - MESSAGE_AREA_PADDING}>
-            <Area ref={ref}>
-                <div>
-                    {state.type === "LOADING" && <Preloader type="ring" />}
-                    {state.type === "WAITING_FOR_NETWORK" && (
-                        <RequiresOnline>
-                            <Preloader type="ring" />
-                        </RequiresOnline>
-                    )}
-                    {state.type === "RENDER" && (
-                        <MessageRenderer id={id} state={state} />
-                    )}
-                    {state.type === "EMPTY" && <ConversationStart id={id} />}
-                </div>
-            </Area>
-        </MessageAreaWidthContext.Provider>
-    );
+	const status = useContext(StatusContext);
+	const { focusTaken } = useContext(IntermediateContext);
+
+	// ? This is the scroll container.
+	const ref = useRef<HTMLDivElement>(null);
+	const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
+
+	// ? Current channel state.
+	const [state, setState] = useState<RenderState>({ type: "LOADING" });
+
+	// ? useRef to avoid re-renders
+	const scrollState = useRef<ScrollState>({ type: "Free" });
+
+	const setScrollState = (v: ScrollState) => {
+		if (v.type === "StayAtBottom") {
+			if (scrollState.current.type === "Bottom" || atBottom()) {
+				scrollState.current = {
+					type: "ScrollToBottom",
+					smooth: v.smooth,
+				};
+			} else {
+				scrollState.current = { type: "Free" };
+			}
+		} else {
+			scrollState.current = v;
+		}
+
+		defer(() => {
+			if (scrollState.current.type === "ScrollToBottom") {
+				setScrollState({
+					type: "Bottom",
+					scrollingUntil: +new Date() + 150,
+				});
+
+				animateScroll.scrollToBottom({
+					container: ref.current,
+					duration: scrollState.current.smooth ? 150 : 0,
+				});
+			} else if (scrollState.current.type === "OffsetTop") {
+				animateScroll.scrollTo(
+					Math.max(
+						101,
+						ref.current.scrollTop +
+							(ref.current.scrollHeight -
+								scrollState.current.previousHeight),
+					),
+					{
+						container: ref.current,
+						duration: 0,
+					},
+				);
+
+				setScrollState({ type: "Free" });
+			} else if (scrollState.current.type === "ScrollTop") {
+				animateScroll.scrollTo(scrollState.current.y, {
+					container: ref.current,
+					duration: 0,
+				});
+
+				setScrollState({ type: "Free" });
+			}
+		});
+	};
+
+	// ? Determine if we are at the bottom of the scroll container.
+	// -> https://stackoverflow.com/a/44893438
+	// By default, we assume we are at the bottom, i.e. when we first load.
+	const atBottom = (offset = 0) =>
+		ref.current
+			? Math.floor(ref.current.scrollHeight - ref.current.scrollTop) -
+					offset <=
+			  ref.current.clientHeight
+			: true;
+
+	const atTop = (offset = 0) => ref.current.scrollTop <= offset;
+
+	// ? Handle events from renderer.
+	useEffect(() => {
+		SingletonMessageRenderer.addListener("state", setState);
+		return () => SingletonMessageRenderer.removeListener("state", setState);
+	}, []);
+
+	useEffect(() => {
+		SingletonMessageRenderer.addListener("scroll", setScrollState);
+		return () =>
+			SingletonMessageRenderer.removeListener("scroll", setScrollState);
+	}, [scrollState]);
+
+	// ? Load channel initially.
+	useEffect(() => {
+		SingletonMessageRenderer.init(id);
+	}, [id]);
+
+	// ? If we are waiting for network, try again.
+	useEffect(() => {
+		switch (status) {
+			case ClientStatus.ONLINE:
+				if (state.type === "WAITING_FOR_NETWORK") {
+					SingletonMessageRenderer.init(id);
+				} else {
+					SingletonMessageRenderer.reloadStale(id);
+				}
+
+				break;
+			case ClientStatus.OFFLINE:
+			case ClientStatus.DISCONNECTED:
+			case ClientStatus.CONNECTING:
+				SingletonMessageRenderer.markStale();
+				break;
+		}
+	}, [status, state]);
+
+	// ? When the container is scrolled.
+	// ? Also handle StayAtBottom
+	useEffect(() => {
+		async function onScroll() {
+			if (scrollState.current.type === "Free" && atBottom()) {
+				setScrollState({ type: "Bottom" });
+			} else if (scrollState.current.type === "Bottom" && !atBottom()) {
+				if (
+					scrollState.current.scrollingUntil &&
+					scrollState.current.scrollingUntil > +new Date()
+				)
+					return;
+				setScrollState({ type: "Free" });
+			}
+		}
+
+		ref.current.addEventListener("scroll", onScroll);
+		return () => ref.current.removeEventListener("scroll", onScroll);
+	}, [ref, scrollState]);
+
+	// ? Top and bottom loaders.
+	useEffect(() => {
+		async function onScroll() {
+			if (atTop(100)) {
+				SingletonMessageRenderer.loadTop(ref.current);
+			}
+
+			if (atBottom(100)) {
+				SingletonMessageRenderer.loadBottom(ref.current);
+			}
+		}
+
+		ref.current.addEventListener("scroll", onScroll);
+		return () => ref.current.removeEventListener("scroll", onScroll);
+	}, [ref]);
+
+	// ? Scroll down whenever the message area resizes.
+	function stbOnResize() {
+		if (!atBottom() && scrollState.current.type === "Bottom") {
+			animateScroll.scrollToBottom({
+				container: ref.current,
+				duration: 0,
+			});
+
+			setScrollState({ type: "Bottom" });
+		}
+	}
+
+	// ? Scroll down when container resized.
+	useLayoutEffect(() => {
+		stbOnResize();
+	}, [height]);
+
+	// ? Scroll down whenever the window resizes.
+	useLayoutEffect(() => {
+		document.addEventListener("resize", stbOnResize);
+		return () => document.removeEventListener("resize", stbOnResize);
+	}, [ref, scrollState]);
+
+	// ? Scroll to bottom when pressing 'Escape'.
+	useEffect(() => {
+		function keyUp(e: KeyboardEvent) {
+			if (e.key === "Escape" && !focusTaken) {
+				SingletonMessageRenderer.jumpToBottom(id, true);
+				internalEmit("TextArea", "focus", "message");
+			}
+		}
+
+		document.body.addEventListener("keyup", keyUp);
+		return () => document.body.removeEventListener("keyup", keyUp);
+	}, [ref, focusTaken]);
+
+	return (
+		<MessageAreaWidthContext.Provider
+			value={(width ?? 0) - MESSAGE_AREA_PADDING}>
+			<Area ref={ref}>
+				<div>
+					{state.type === "LOADING" && <Preloader type="ring" />}
+					{state.type === "WAITING_FOR_NETWORK" && (
+						<RequiresOnline>
+							<Preloader type="ring" />
+						</RequiresOnline>
+					)}
+					{state.type === "RENDER" && (
+						<MessageRenderer id={id} state={state} />
+					)}
+					{state.type === "EMPTY" && <ConversationStart id={id} />}
+				</div>
+			</Area>
+		</MessageAreaWidthContext.Provider>
+	);
 }
diff --git a/src/pages/channels/messaging/MessageEditor.tsx b/src/pages/channels/messaging/MessageEditor.tsx
index 7e8ecd17195f28cbc79825a1bdff7dd8c2c82fda..78fe70bb4ef7752d6dca89983d671042207076cf 100644
--- a/src/pages/channels/messaging/MessageEditor.tsx
+++ b/src/pages/channels/messaging/MessageEditor.tsx
@@ -1,115 +1,133 @@
 import styled from "styled-components";
-import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
-import { MessageObject } from "../../../context/revoltjs/util";
+
 import { useContext, useEffect, useState } from "preact/hooks";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
 import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
-import { IntermediateContext, useIntermediate } from "../../../context/intermediate/Intermediate";
-import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
+
+import {
+	IntermediateContext,
+	useIntermediate,
+} from "../../../context/intermediate/Intermediate";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import { MessageObject } from "../../../context/revoltjs/util";
+
+import AutoComplete, {
+	useAutoComplete,
+} from "../../../components/common/AutoComplete";
 
 const EditorBase = styled.div`
-    display: flex;
-    flex-direction: column;
-
-    textarea {
-        resize: none;
-        padding: 12px;
-        font-size: .875rem;
-        border-radius: 3px;
-        white-space: pre-wrap;
-        background: var(--secondary-header);
-    }
-
-    .caption {
-        padding: 2px;
-        font-size: 11px;
-        color: var(--tertiary-foreground);
-
-        a {
-            cursor: pointer;
-            &:hover {
-                text-decoration: underline;
-            }
-        }
-    }
+	display: flex;
+	flex-direction: column;
+
+	textarea {
+		resize: none;
+		padding: 12px;
+		font-size: 0.875rem;
+		border-radius: 3px;
+		white-space: pre-wrap;
+		background: var(--secondary-header);
+	}
+
+	.caption {
+		padding: 2px;
+		font-size: 11px;
+		color: var(--tertiary-foreground);
+
+		a {
+			cursor: pointer;
+			&:hover {
+				text-decoration: underline;
+			}
+		}
+	}
 `;
 
 interface Props {
-    message: MessageObject
-    finish: () => void
+	message: MessageObject;
+	finish: () => void;
 }
 
 export default function MessageEditor({ message, finish }: Props) {
-    const [ content, setContent ] = useState(message.content as string ?? '');
-    const { focusTaken } = useContext(IntermediateContext);
-    const { openScreen } = useIntermediate();
-    const client = useContext(AppContext);
-
-    async function save() {
-        finish();
-
-        if (content.length === 0) {
-            // @ts-expect-error
-            openScreen({ id: 'special_prompt', type: 'delete_message', target: message });
-        } else if (content !== message.content) {
-            await client.channels.editMessage(
-                message.channel,
-                message._id,
-                { content }
-            );
-        }
-    }
-
-    // ? Stop editing when pressing ESC.
-    useEffect(() => {
-        function keyUp(e: KeyboardEvent) {
-            if (e.key === "Escape" && !focusTaken) {
-                finish();
-            }
-        }
-
-        document.body.addEventListener("keyup", keyUp);
-        return () => document.body.removeEventListener("keyup", keyUp);
-    }, [focusTaken]);
-
-    const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } =
-        useAutoComplete(v => setContent(v ?? ''), {
-            users: { type: 'all' }
-        });
-
-    return (
-        <EditorBase>
-            <AutoComplete detached {...autoCompleteProps} />
-            <TextAreaAutoSize
-                forceFocus
-                maxRows={3}
-                padding={12}
-                value={content}
-                maxLength={2000}
-                onChange={ev => {
-                    onChange(ev);
-                    setContent(ev.currentTarget.value)
-                }}
-                onKeyDown={e => {
-                    if (onKeyDown(e)) return;
-
-                    if (
-                        !e.shiftKey &&
-                        e.key === "Enter" &&
-                        !isTouchscreenDevice
-                    ) {
-                        e.preventDefault();
-                        save();
-                    }
-                }}
-                onKeyUp={onKeyUp}
-                onFocus={onFocus}
-                onBlur={onBlur}
-            />
-            <span className="caption">
-                escape to <a onClick={finish}>cancel</a> &middot;
-                enter to <a onClick={save}>save</a>
-            </span>
-        </EditorBase>
-    )
+	const [content, setContent] = useState((message.content as string) ?? "");
+	const { focusTaken } = useContext(IntermediateContext);
+	const { openScreen } = useIntermediate();
+	const client = useContext(AppContext);
+
+	async function save() {
+		finish();
+
+		if (content.length === 0) {
+			openScreen({
+				id: "special_prompt",
+				// @ts-expect-error
+				type: "delete_message",
+				// @ts-expect-error
+				target: message,
+			});
+		} else if (content !== message.content) {
+			await client.channels.editMessage(message.channel, message._id, {
+				content,
+			});
+		}
+	}
+
+	// ? Stop editing when pressing ESC.
+	useEffect(() => {
+		function keyUp(e: KeyboardEvent) {
+			if (e.key === "Escape" && !focusTaken) {
+				finish();
+			}
+		}
+
+		document.body.addEventListener("keyup", keyUp);
+		return () => document.body.removeEventListener("keyup", keyUp);
+	}, [focusTaken]);
+
+	const {
+		onChange,
+		onKeyUp,
+		onKeyDown,
+		onFocus,
+		onBlur,
+		...autoCompleteProps
+	} = useAutoComplete((v) => setContent(v ?? ""), {
+		users: { type: "all" },
+	});
+
+	return (
+		<EditorBase>
+			<AutoComplete detached {...autoCompleteProps} />
+			<TextAreaAutoSize
+				forceFocus
+				maxRows={3}
+				padding={12}
+				value={content}
+				maxLength={2000}
+				onChange={(ev) => {
+					onChange(ev);
+					setContent(ev.currentTarget.value);
+				}}
+				onKeyDown={(e) => {
+					if (onKeyDown(e)) return;
+
+					if (
+						!e.shiftKey &&
+						e.key === "Enter" &&
+						!isTouchscreenDevice
+					) {
+						e.preventDefault();
+						save();
+					}
+				}}
+				onKeyUp={onKeyUp}
+				onFocus={onFocus}
+				onBlur={onBlur}
+			/>
+			<span className="caption">
+				escape to <a onClick={finish}>cancel</a> &middot; enter to{" "}
+				<a onClick={save}>save</a>
+			</span>
+		</EditorBase>
+	);
 }
diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx
index 0f01f2e2b0354819d252db40b0d0f0022f4bb217..1a9ae45a21cc52e3a4b4fc51aede9a8dee840e02 100644
--- a/src/pages/channels/messaging/MessageRenderer.tsx
+++ b/src/pages/channels/messaging/MessageRenderer.tsx
@@ -1,198 +1,215 @@
+import { X } from "@styled-icons/boxicons-regular";
+import { Users } from "revolt.js/dist/api/objects";
+import styled from "styled-components";
 import { decodeTime } from "ulid";
+
 import { memo } from "preact/compat";
-import styled from "styled-components";
-import MessageEditor from "./MessageEditor";
-import { Children } from "../../../types/Preact";
-import { Users } from "revolt.js/dist/api/objects";
-import { X } from "@styled-icons/boxicons-regular";
-import ConversationStart from "./ConversationStart";
-import { connectState } from "../../../redux/connector";
-import Preloader from "../../../components/ui/Preloader";
+import { useContext, useEffect, useState } from "preact/hooks";
+
+import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
 import { RenderState } from "../../../lib/renderer/types";
-import DateDivider from "../../../components/ui/DateDivider";
+
+import { connectState } from "../../../redux/connector";
 import { QueuedMessage } from "../../../redux/reducers/queue";
-import { useContext, useEffect, useState } from "preact/hooks";
+
+import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
 import { MessageObject } from "../../../context/revoltjs/util";
+
 import Message from "../../../components/common/messaging/Message";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
-import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
-import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
 import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
+import DateDivider from "../../../components/ui/DateDivider";
+import Preloader from "../../../components/ui/Preloader";
+
+import { Children } from "../../../types/Preact";
+import ConversationStart from "./ConversationStart";
+import MessageEditor from "./MessageEditor";
 
 interface Props {
-    id: string;
-    state: RenderState;
-    queue: QueuedMessage[];
+	id: string;
+	state: RenderState;
+	queue: QueuedMessage[];
 }
 
 const BlockedMessage = styled.div`
-    font-size: 0.8em;
-    margin-top: 6px;
-    padding: 4px 64px;
-    color: var(--tertiary-foreground);
-
-    &:hover {
-        background: var(--hover);
-    }
+	font-size: 0.8em;
+	margin-top: 6px;
+	padding: 4px 64px;
+	color: var(--tertiary-foreground);
+
+	&:hover {
+		background: var(--hover);
+	}
 `;
 
 function MessageRenderer({ id, state, queue }: Props) {
-    if (state.type !== 'RENDER') return null;
-
-    const client = useContext(AppContext);
-    const userId = client.user!._id;
-
-    const [editing, setEditing] = useState<string | undefined>(undefined);
-    const stopEditing = () => {
-        setEditing(undefined);
-        internalEmit("TextArea", "focus", "message");
-    };
-
-    useEffect(() => {
-        function editLast() {
-            if (state.type !== 'RENDER') return;
-            for (let i = state.messages.length - 1; i >= 0; i--) {
-                if (state.messages[i].author === userId) {
-                    setEditing(state.messages[i]._id);
-                    return;
-                }
-            }
-        }
-
-        const subs = [
-            internalSubscribe("MessageRenderer", "edit_last", editLast),
-            internalSubscribe("MessageRenderer", "edit_message", setEditing)
-        ]
-
-        return () => subs.forEach(unsub => unsub());
-    }, [state.messages]);
-
-    let render: Children[] = [],
-        previous: MessageObject | undefined;
-
-    if (state.atTop) {
-        render.push(<ConversationStart id={id} />);
-    } else {
-        render.push(
-            <RequiresOnline>
-                <Preloader type="ring" />
-            </RequiresOnline>
-        );
-    }
-
-    let head = true;
-    function compare(
-        current: string,
-        curAuthor: string,
-        previous: string,
-        prevAuthor: string
-    ) {
-        const atime = decodeTime(current),
-            adate = new Date(atime),
-            btime = decodeTime(previous),
-            bdate = new Date(btime);
-
-        if (
-            adate.getFullYear() !== bdate.getFullYear() ||
-            adate.getMonth() !== bdate.getMonth() ||
-            adate.getDate() !== bdate.getDate()
-        ) {
-            render.push(<DateDivider date={adate} />);
-            head = true;
-        }
-
-        head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
-    }
-
-    let blocked = 0;
-    function pushBlocked() {
-        render.push(<BlockedMessage><X size={16} /> { blocked } blocked messages</BlockedMessage>);
-        blocked = 0;
-    }
-
-    for (const message of state.messages) {
-        if (previous) {
-            compare(
-                message._id,
-                message.author,
-                previous._id,
-                previous.author
-            );
-        }
-
-        if (message.author === "00000000000000000000000000") {
-            render.push(<SystemMessage key={message._id} message={message} attachContext />);
-        } else {
-            // ! FIXME: temp solution
-            if (client.users.get(message.author)?.relationship === Users.Relationship.Blocked) {
-                blocked++;
-            } else {
-                if (blocked > 0) pushBlocked();
-
-                render.push(
-                    <Message message={message}
-                        key={message._id}
-                        head={head}
-                        content={
-                            editing === message._id ?
-                                <MessageEditor message={message} finish={stopEditing} />
-                                : undefined
-                        }
-                        attachContext />
-                );
-            }
-        }
-
-        previous = message;
-    }
-    
-    if (blocked > 0) pushBlocked();
-
-    const nonces = state.messages.map(x => x.nonce);
-    if (state.atBottom) {
-        for (const msg of queue) {
-            if (msg.channel !== id) continue;
-            if (nonces.includes(msg.id)) continue;
-
-            if (previous) {
-                compare(
-                    msg.id,
-                    userId!,
-                    previous._id,
-                    previous.author
-                );
-                
-                previous = {
-                    _id: msg.id,
-                    data: { author: userId! }
-                } as any;
-            }
-
-            render.push(
-                <Message
-                    message={{
-                        ...msg.data,
-                        replies: msg.data.replies.map(x => x.id)
-                    }}
-                    key={msg.id}
-                    queued={msg}
-                    head={head}
-                    attachContext />
-            );
-        }
-    } else {
-        render.push(
-            <RequiresOnline>
-                <Preloader type="ring" />
-            </RequiresOnline>
-        );
-    }
-
-    return <>{ render }</>;
+	if (state.type !== "RENDER") return null;
+
+	const client = useContext(AppContext);
+	const userId = client.user!._id;
+
+	const [editing, setEditing] = useState<string | undefined>(undefined);
+	const stopEditing = () => {
+		setEditing(undefined);
+		internalEmit("TextArea", "focus", "message");
+	};
+
+	useEffect(() => {
+		function editLast() {
+			if (state.type !== "RENDER") return;
+			for (let i = state.messages.length - 1; i >= 0; i--) {
+				if (state.messages[i].author === userId) {
+					setEditing(state.messages[i]._id);
+					return;
+				}
+			}
+		}
+
+		const subs = [
+			internalSubscribe("MessageRenderer", "edit_last", editLast),
+			internalSubscribe("MessageRenderer", "edit_message", setEditing),
+		];
+
+		return () => subs.forEach((unsub) => unsub());
+	}, [state.messages]);
+
+	let render: Children[] = [],
+		previous: MessageObject | undefined;
+
+	if (state.atTop) {
+		render.push(<ConversationStart id={id} />);
+	} else {
+		render.push(
+			<RequiresOnline>
+				<Preloader type="ring" />
+			</RequiresOnline>,
+		);
+	}
+
+	let head = true;
+	function compare(
+		current: string,
+		curAuthor: string,
+		previous: string,
+		prevAuthor: string,
+	) {
+		const atime = decodeTime(current),
+			adate = new Date(atime),
+			btime = decodeTime(previous),
+			bdate = new Date(btime);
+
+		if (
+			adate.getFullYear() !== bdate.getFullYear() ||
+			adate.getMonth() !== bdate.getMonth() ||
+			adate.getDate() !== bdate.getDate()
+		) {
+			render.push(<DateDivider date={adate} />);
+			head = true;
+		}
+
+		head = curAuthor !== prevAuthor || Math.abs(btime - atime) >= 420000;
+	}
+
+	let blocked = 0;
+	function pushBlocked() {
+		render.push(
+			<BlockedMessage>
+				<X size={16} /> {blocked} blocked messages
+			</BlockedMessage>,
+		);
+		blocked = 0;
+	}
+
+	for (const message of state.messages) {
+		if (previous) {
+			compare(message._id, message.author, previous._id, previous.author);
+		}
+
+		if (message.author === "00000000000000000000000000") {
+			render.push(
+				<SystemMessage
+					key={message._id}
+					message={message}
+					attachContext
+				/>,
+			);
+		} else {
+			// ! FIXME: temp solution
+			if (
+				client.users.get(message.author)?.relationship ===
+				Users.Relationship.Blocked
+			) {
+				blocked++;
+			} else {
+				if (blocked > 0) pushBlocked();
+
+				render.push(
+					<Message
+						message={message}
+						key={message._id}
+						head={head}
+						content={
+							editing === message._id ? (
+								<MessageEditor
+									message={message}
+									finish={stopEditing}
+								/>
+							) : undefined
+						}
+						attachContext
+					/>,
+				);
+			}
+		}
+
+		previous = message;
+	}
+
+	if (blocked > 0) pushBlocked();
+
+	const nonces = state.messages.map((x) => x.nonce);
+	if (state.atBottom) {
+		for (const msg of queue) {
+			if (msg.channel !== id) continue;
+			if (nonces.includes(msg.id)) continue;
+
+			if (previous) {
+				compare(msg.id, userId!, previous._id, previous.author);
+
+				previous = {
+					_id: msg.id,
+					data: { author: userId! },
+				} as any;
+			}
+
+			render.push(
+				<Message
+					message={{
+						...msg.data,
+						replies: msg.data.replies.map((x) => x.id),
+					}}
+					key={msg.id}
+					queued={msg}
+					head={head}
+					attachContext
+				/>,
+			);
+		}
+	} else {
+		render.push(
+			<RequiresOnline>
+				<Preloader type="ring" />
+			</RequiresOnline>,
+		);
+	}
+
+	return <>{render}</>;
 }
 
-export default memo(connectState<Omit<Props, 'queue'>>(MessageRenderer, state => {
-    return {
-        queue: state.queue
-    };
-}));
+export default memo(
+	connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
+		return {
+			queue: state.queue,
+		};
+	}),
+);
diff --git a/src/pages/channels/voice/VoiceHeader.tsx b/src/pages/channels/voice/VoiceHeader.tsx
index 16894650e1841fb363ff675e542b014bb2b2ec4d..106e27d8b1b5bae8f28882a4b3d638e96d120cdd 100644
--- a/src/pages/channels/voice/VoiceHeader.tsx
+++ b/src/pages/channels/voice/VoiceHeader.tsx
@@ -1,117 +1,138 @@
-import { Text } from "preact-i18n";
+import { BarChart } from "@styled-icons/boxicons-regular";
 import styled from "styled-components";
+
+import { Text } from "preact-i18n";
 import { useContext } from "preact/hooks";
-import { BarChart } from "@styled-icons/boxicons-regular";
-import Button from "../../../components/ui/Button";
+
+import {
+	VoiceContext,
+	VoiceOperationsContext,
+	VoiceStatus,
+} from "../../../context/Voice";
+import {
+	useForceUpdate,
+	useSelf,
+	useUsers,
+} from "../../../context/revoltjs/hooks";
+
 import UserIcon from "../../../components/common/user/UserIcon";
-import { useForceUpdate, useSelf, useUsers } from "../../../context/revoltjs/hooks";
-import { VoiceContext, VoiceOperationsContext, VoiceStatus } from "../../../context/Voice";
+import Button from "../../../components/ui/Button";
 
 interface Props {
-    id: string
+	id: string;
 }
 
 const VoiceBase = styled.div`
-    padding: 20px;
-    background: var(--secondary-background);
-
-    .status {
-        position: absolute;
-        color: var(--success);
-        background: var(--primary-background);
-        display: flex;
-        align-items: center;
-        padding: 10px;
-        font-size: 14px;
-        font-weight: 600;
-        border-radius: 7px;
-        flex: 1 0;
-        user-select: none;
-
-        svg {
-            margin-inline-end: 4px;
-            cursor: help;
-        }
-    }
-
-    display: flex;
-    flex-direction: column;
-
-    .participants {
-        margin: 20px 0;
-        justify-content: center;
-        pointer-events: none;
-        user-select: none;
-        display: flex;
-        gap: 16px;
-
-        .disconnected {
-            opacity: 0.5;
-        }
-    }
-
-    .actions {
-        display: flex;
-        justify-content: center;
-        gap: 10px;
-    }
+	padding: 20px;
+	background: var(--secondary-background);
+
+	.status {
+		position: absolute;
+		color: var(--success);
+		background: var(--primary-background);
+		display: flex;
+		align-items: center;
+		padding: 10px;
+		font-size: 14px;
+		font-weight: 600;
+		border-radius: 7px;
+		flex: 1 0;
+		user-select: none;
+
+		svg {
+			margin-inline-end: 4px;
+			cursor: help;
+		}
+	}
+
+	display: flex;
+	flex-direction: column;
+
+	.participants {
+		margin: 20px 0;
+		justify-content: center;
+		pointer-events: none;
+		user-select: none;
+		display: flex;
+		gap: 16px;
+
+		.disconnected {
+			opacity: 0.5;
+		}
+	}
+
+	.actions {
+		display: flex;
+		justify-content: center;
+		gap: 10px;
+	}
 `;
 
 export default function VoiceHeader({ id }: Props) {
-    const { status, participants, roomId } = useContext(VoiceContext);
-    if (roomId !== id) return null;
-
-    const { isProducing, startProducing, stopProducing, disconnect } = useContext(VoiceOperationsContext);
-
-    const ctx = useForceUpdate();
-    const self = useSelf(ctx);
-    const keys = participants ? Array.from(participants.keys()) : undefined;
-    const users = keys ? useUsers(keys, ctx) : undefined;
-    
-    return (
-        <VoiceBase>
-            <div className="participants">
-                { users && users.length !== 0 ? users.map((user, index) => {
-                    const id = keys![index];
-                    return (
-                        <div key={id}>
-                            <UserIcon
-                                size={80}
-                                target={user}
-                                status={false}
-                                voice={ participants!.get(id)?.audio ? undefined : "muted" }
-                            />
-                        </div>
-                    );
-                }) : self !== undefined && (
-                    <div key={self._id} className="disconnected">
-                        <UserIcon
-                            size={80}
-                            target={self}
-                            status={false} />
-                    </div>
-                )}
-            </div>
-            <div className="status">
-                <BarChart size={20} />
-                { status === VoiceStatus.CONNECTED && <Text id="app.main.channel.voice.connected" /> }
-            </div>
-            <div className="actions">
-                <Button error onClick={disconnect}>
-                    <Text id="app.main.channel.voice.leave" />
-                </Button>
-                { isProducing("audio") ? (
-                    <Button onClick={() => stopProducing("audio")}>
-                        <Text id="app.main.channel.voice.mute" />
-                    </Button>
-                ) : (
-                    <Button onClick={() => startProducing("audio")}>
-                        <Text id="app.main.channel.voice.unmute" />
-                    </Button>
-                )}
-            </div>
-        </VoiceBase>
-    )
+	const { status, participants, roomId } = useContext(VoiceContext);
+	if (roomId !== id) return null;
+
+	const { isProducing, startProducing, stopProducing, disconnect } =
+		useContext(VoiceOperationsContext);
+
+	const ctx = useForceUpdate();
+	const self = useSelf(ctx);
+	const keys = participants ? Array.from(participants.keys()) : undefined;
+	const users = keys ? useUsers(keys, ctx) : undefined;
+
+	return (
+		<VoiceBase>
+			<div className="participants">
+				{users && users.length !== 0
+					? users.map((user, index) => {
+							const id = keys![index];
+							return (
+								<div key={id}>
+									<UserIcon
+										size={80}
+										target={user}
+										status={false}
+										voice={
+											participants!.get(id)?.audio
+												? undefined
+												: "muted"
+										}
+									/>
+								</div>
+							);
+					  })
+					: self !== undefined && (
+							<div key={self._id} className="disconnected">
+								<UserIcon
+									size={80}
+									target={self}
+									status={false}
+								/>
+							</div>
+					  )}
+			</div>
+			<div className="status">
+				<BarChart size={20} />
+				{status === VoiceStatus.CONNECTED && (
+					<Text id="app.main.channel.voice.connected" />
+				)}
+			</div>
+			<div className="actions">
+				<Button error onClick={disconnect}>
+					<Text id="app.main.channel.voice.leave" />
+				</Button>
+				{isProducing("audio") ? (
+					<Button onClick={() => stopProducing("audio")}>
+						<Text id="app.main.channel.voice.mute" />
+					</Button>
+				) : (
+					<Button onClick={() => startProducing("audio")}>
+						<Text id="app.main.channel.voice.unmute" />
+					</Button>
+				)}
+			</div>
+		</VoiceBase>
+	);
 }
 
 /**{voice.roomId === id && (
diff --git a/src/pages/developer/Developer.tsx b/src/pages/developer/Developer.tsx
index e8c3e187da029c0aeff7ea6dfad48cf1d5cccc8c..6e39a16ad6be51d4d466d1cc9e12879fde754312 100644
--- a/src/pages/developer/Developer.tsx
+++ b/src/pages/developer/Developer.tsx
@@ -1,34 +1,41 @@
+import { Wrench } from "@styled-icons/boxicons-solid";
+
 import { useContext } from "preact/hooks";
-import { TextReact } from "../../lib/i18n";
-import Header from "../../components/ui/Header";
+
 import PaintCounter from "../../lib/PaintCounter";
+import { TextReact } from "../../lib/i18n";
+
 import { AppContext } from "../../context/revoltjs/RevoltClient";
 import { useUserPermission } from "../../context/revoltjs/hooks";
-import { Wrench } from "@styled-icons/boxicons-solid";
+
+import Header from "../../components/ui/Header";
 
 export default function Developer() {
-    // const voice = useContext(VoiceContext);
-    const client = useContext(AppContext);
-    const userPermission = useUserPermission(client.user!._id);
+	// const voice = useContext(VoiceContext);
+	const client = useContext(AppContext);
+	const userPermission = useUserPermission(client.user!._id);
 
-    return (
-        <div>
-            <Header placement="primary">
-                <Wrench size="24" />
-                Developer Tab
-            </Header>
-            <div style={{ padding: "16px" }}>
-                <PaintCounter always />
-            </div>
-            <div style={{ padding: "16px" }}>
-                <b>User ID:</b> {client.user!._id} <br/>
-                <b>Permission against self:</b> {userPermission} <br/>
-            </div>
-            <div style={{ padding: "16px" }}>
-                <TextReact id="login.open_mail_provider" fields={{ provider: <b>GAMING!</b> }} />
-            </div>
-            <div style={{ padding: "16px" }}>
-                {/*<span>
+	return (
+		<div>
+			<Header placement="primary">
+				<Wrench size="24" />
+				Developer Tab
+			</Header>
+			<div style={{ padding: "16px" }}>
+				<PaintCounter always />
+			</div>
+			<div style={{ padding: "16px" }}>
+				<b>User ID:</b> {client.user!._id} <br />
+				<b>Permission against self:</b> {userPermission} <br />
+			</div>
+			<div style={{ padding: "16px" }}>
+				<TextReact
+					id="login.open_mail_provider"
+					fields={{ provider: <b>GAMING!</b> }}
+				/>
+			</div>
+			<div style={{ padding: "16px" }}>
+				{/*<span>
                     <b>Voice Status:</b> {VoiceStatus[voice.status]}
                 </span>
                 <br />
@@ -41,7 +48,7 @@ export default function Developer() {
                     {Array.from(voice.participants.keys()).join(", ")}]
                 </span>
                 <br />*/}
-            </div>
-        </div>
-    );
+			</div>
+		</div>
+	);
 }
diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx
index d2a795e6f809c17d54904d9fdd89f959b168de9c..2632dcca64e0680917b0c3a79112b17ad11b24b8 100644
--- a/src/pages/friends/Friend.tsx
+++ b/src/pages/friends/Friend.tsx
@@ -1,107 +1,136 @@
-import { Text } from "preact-i18n";
-import classNames from "classnames";
-import styles from "./Friend.module.scss";
-import { useContext } from "preact/hooks";
-import { Children } from "../../types/Preact";
-import IconButton from "../../components/ui/IconButton";
-import { attachContextMenu } from "preact-context-menu";
 import { X, Plus } from "@styled-icons/boxicons-regular";
+import { PhoneCall, Envelope } from "@styled-icons/boxicons-solid";
 import { User, Users } from "revolt.js/dist/api/objects";
+
+import styles from "./Friend.module.scss";
+import classNames from "classnames";
+import { attachContextMenu } from "preact-context-menu";
+import { Text } from "preact-i18n";
+import { useContext } from "preact/hooks";
+
 import { stopPropagation } from "../../lib/stopPropagation";
+
 import { VoiceOperationsContext } from "../../context/Voice";
-import UserIcon from "../../components/common/user/UserIcon";
-import UserStatus from '../../components/common/user/UserStatus';
-import { PhoneCall, Envelope } from "@styled-icons/boxicons-solid";
 import { useIntermediate } from "../../context/intermediate/Intermediate";
-import { AppContext, OperationsContext } from "../../context/revoltjs/RevoltClient";
+import {
+	AppContext,
+	OperationsContext,
+} from "../../context/revoltjs/RevoltClient";
+
+import UserIcon from "../../components/common/user/UserIcon";
+import UserStatus from "../../components/common/user/UserStatus";
+import IconButton from "../../components/ui/IconButton";
+
+import { Children } from "../../types/Preact";
 
 interface Props {
-    user: User;
+	user: User;
 }
 
 export function Friend({ user }: Props) {
-    const client = useContext(AppContext);
-    const { openScreen } = useIntermediate();
-    const { openDM } = useContext(OperationsContext);
-    const { connect } = useContext(VoiceOperationsContext);
+	const client = useContext(AppContext);
+	const { openScreen } = useIntermediate();
+	const { openDM } = useContext(OperationsContext);
+	const { connect } = useContext(VoiceOperationsContext);
 
-    const actions: Children[] = [];
-    let subtext: Children = null;
+	const actions: Children[] = [];
+	let subtext: Children = null;
 
-    if (user.relationship === Users.Relationship.Friend) {
-        subtext = <UserStatus user={user} />
-        actions.push(
-            <>
-                <IconButton type="circle"
-                    className={classNames(styles.button, styles.call, styles.success)}
-                    onClick={ev => stopPropagation(ev, openDM(user._id).then(connect))}>
-                    <PhoneCall size={20} />
-                </IconButton>
-                <IconButton type="circle"
-                    className={styles.button}
-                    onClick={ev => stopPropagation(ev, openDM(user._id))}>
-                    <Envelope size={20} />
-                </IconButton>
-            </>
-        );
-    }
+	if (user.relationship === Users.Relationship.Friend) {
+		subtext = <UserStatus user={user} />;
+		actions.push(
+			<>
+				<IconButton
+					type="circle"
+					className={classNames(
+						styles.button,
+						styles.call,
+						styles.success,
+					)}
+					onClick={(ev) =>
+						stopPropagation(ev, openDM(user._id).then(connect))
+					}>
+					<PhoneCall size={20} />
+				</IconButton>
+				<IconButton
+					type="circle"
+					className={styles.button}
+					onClick={(ev) => stopPropagation(ev, openDM(user._id))}>
+					<Envelope size={20} />
+				</IconButton>
+			</>,
+		);
+	}
 
-    if (user.relationship === Users.Relationship.Incoming) {
-        actions.push(
-            <IconButton type="circle"
-                className={styles.button}
-                onClick={ev => stopPropagation(ev, client.users.addFriend(user.username))}>
-                <Plus size={24} />
-            </IconButton>
-        );
+	if (user.relationship === Users.Relationship.Incoming) {
+		actions.push(
+			<IconButton
+				type="circle"
+				className={styles.button}
+				onClick={(ev) =>
+					stopPropagation(ev, client.users.addFriend(user.username))
+				}>
+				<Plus size={24} />
+			</IconButton>,
+		);
 
-        subtext = <Text id="app.special.friends.incoming" />;
-    }
+		subtext = <Text id="app.special.friends.incoming" />;
+	}
 
-    if (user.relationship === Users.Relationship.Outgoing) {
-        subtext = <Text id="app.special.friends.outgoing" />;
-    }
+	if (user.relationship === Users.Relationship.Outgoing) {
+		subtext = <Text id="app.special.friends.outgoing" />;
+	}
 
-    if (
-        user.relationship === Users.Relationship.Friend ||
-        user.relationship === Users.Relationship.Outgoing ||
-        user.relationship === Users.Relationship.Incoming
-    ) {
-        actions.push(
-            <IconButton type="circle"
-                className={classNames(styles.button, styles.error)}
-                onClick={ev => stopPropagation(ev,
-                    user.relationship === Users.Relationship.Friend ?
-                        openScreen({ id: 'special_prompt', type: 'unfriend_user', target: user })
-                        : client.users.removeFriend(user._id)
-                )}>
-                <X size={24} />
-            </IconButton>
-        );
-    }
+	if (
+		user.relationship === Users.Relationship.Friend ||
+		user.relationship === Users.Relationship.Outgoing ||
+		user.relationship === Users.Relationship.Incoming
+	) {
+		actions.push(
+			<IconButton
+				type="circle"
+				className={classNames(styles.button, styles.error)}
+				onClick={(ev) =>
+					stopPropagation(
+						ev,
+						user.relationship === Users.Relationship.Friend
+							? openScreen({
+									id: "special_prompt",
+									type: "unfriend_user",
+									target: user,
+							  })
+							: client.users.removeFriend(user._id),
+					)
+				}>
+				<X size={24} />
+			</IconButton>,
+		);
+	}
 
-    if (user.relationship === Users.Relationship.Blocked) {
-        actions.push(
-            <IconButton type="circle"
-                className={classNames(styles.button, styles.error)}
-                onClick={ev => stopPropagation(ev, client.users.unblockUser(user._id))}>
-                <X size={24} />
-            </IconButton>
-        );
-    }
+	if (user.relationship === Users.Relationship.Blocked) {
+		actions.push(
+			<IconButton
+				type="circle"
+				className={classNames(styles.button, styles.error)}
+				onClick={(ev) =>
+					stopPropagation(ev, client.users.unblockUser(user._id))
+				}>
+				<X size={24} />
+			</IconButton>,
+		);
+	}
 
-    return (
-        <div className={styles.friend}
-            onClick={() => openScreen({ id: 'profile', user_id: user._id })}
-            onContextMenu={attachContextMenu('Menu', { user: user._id })}>
-            <UserIcon target={user} size={36} status />
-            <div className={styles.name}>
-                <span>@{user.username}</span>
-                {subtext && (
-                    <span className={styles.subtext}>{subtext}</span>
-                )}
-            </div>
-            <div className={styles.actions}>{actions}</div>
-        </div>
-    );
+	return (
+		<div
+			className={styles.friend}
+			onClick={() => openScreen({ id: "profile", user_id: user._id })}
+			onContextMenu={attachContextMenu("Menu", { user: user._id })}>
+			<UserIcon target={user} size={36} status />
+			<div className={styles.name}>
+				<span>@{user.username}</span>
+				{subtext && <span className={styles.subtext}>{subtext}</span>}
+			</div>
+			<div className={styles.actions}>{actions}</div>
+		</div>
+	);
 }
diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx
index d90b5d58781dbe7b70a539bf8c4e1b79b5392991..6b322e5b108dcd3e8b395fa7aee5f9a90dd921d6 100644
--- a/src/pages/friends/Friends.tsx
+++ b/src/pages/friends/Friends.tsx
@@ -1,78 +1,117 @@
-import { Friend } from "./Friend";
-import { Text } from "preact-i18n";
-import styles from "./Friend.module.scss";
-import Header from "../../components/ui/Header";
-import Overline from "../../components/ui/Overline";
-import Tooltip from "../../components/common/Tooltip";
-import IconButton from "../../components/ui/IconButton";
-import { useUsers } from "../../context/revoltjs/hooks";
+import {
+	ChevronDown,
+	ChevronRight,
+	ListPlus,
+} from "@styled-icons/boxicons-regular";
+import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
 import { User, Users } from "revolt.js/dist/api/objects";
-import UserIcon from "../../components/common/user/UserIcon";
+
+import styles from "./Friend.module.scss";
+import { Text } from "preact-i18n";
+
+import { TextReact } from "../../lib/i18n";
 import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+
 import { useIntermediate } from "../../context/intermediate/Intermediate";
-import { ChevronDown, ChevronRight, ListPlus } from "@styled-icons/boxicons-regular";
-import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
-import { TextReact } from "../../lib/i18n";
-import { Children } from "../../types/Preact";
-import Details from "../../components/ui/Details";
+import { useUsers } from "../../context/revoltjs/hooks";
+
 import CollapsibleSection from "../../components/common/CollapsibleSection";
+import Tooltip from "../../components/common/Tooltip";
+import UserIcon from "../../components/common/user/UserIcon";
+import Details from "../../components/ui/Details";
+import Header from "../../components/ui/Header";
+import IconButton from "../../components/ui/IconButton";
+import Overline from "../../components/ui/Overline";
+
+import { Children } from "../../types/Preact";
+import { Friend } from "./Friend";
 
 export default function Friends() {
-    const { openScreen } = useIntermediate();
+	const { openScreen } = useIntermediate();
 
-    const users = useUsers() as User[];
-    users.sort((a, b) => a.username.localeCompare(b.username));
+	const users = useUsers() as User[];
+	users.sort((a, b) => a.username.localeCompare(b.username));
 
-    const friends = users.filter(x => x.relationship === Users.Relationship.Friend);
+	const friends = users.filter(
+		(x) => x.relationship === Users.Relationship.Friend,
+	);
 
-    const lists = [
-        [ '', users.filter(x =>
-            x.relationship === Users.Relationship.Incoming
-        ) ],
-        [ 'app.special.friends.sent', users.filter(x =>
-            x.relationship === Users.Relationship.Outgoing
-        ), 'outgoing' ],
-        [ 'app.status.online', friends.filter(x =>
-            x.online && x.status?.presence !== Users.Presence.Invisible
-        ), 'online' ],
-        [ 'app.status.offline', friends.filter(x =>
-            !x.online || x.status?.presence === Users.Presence.Invisible
-        ), 'offline' ],
-        [ 'app.special.friends.blocked', users.filter(x =>
-            x.relationship === Users.Relationship.Blocked
-        ), 'blocked' ]
-    ] as [ string, User[], string ][];
+	const lists = [
+		[
+			"",
+			users.filter((x) => x.relationship === Users.Relationship.Incoming),
+		],
+		[
+			"app.special.friends.sent",
+			users.filter((x) => x.relationship === Users.Relationship.Outgoing),
+			"outgoing",
+		],
+		[
+			"app.status.online",
+			friends.filter(
+				(x) =>
+					x.online && x.status?.presence !== Users.Presence.Invisible,
+			),
+			"online",
+		],
+		[
+			"app.status.offline",
+			friends.filter(
+				(x) =>
+					!x.online ||
+					x.status?.presence === Users.Presence.Invisible,
+			),
+			"offline",
+		],
+		[
+			"app.special.friends.blocked",
+			users.filter((x) => x.relationship === Users.Relationship.Blocked),
+			"blocked",
+		],
+	] as [string, User[], string][];
 
-    const incoming = lists[0][1];
-    const userlist: Children[] = incoming.map(x => <b>{ x.username }</b>);
-    for (let i=incoming.length-1;i>0;i--) userlist.splice(i, 0, ', ');
+	const incoming = lists[0][1];
+	const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>);
+	for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
 
-    const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
-    return (
-        <>
-            <Header placement="primary">
-                { !isTouchscreenDevice && <UserDetail size={24} /> }
-                <div className={styles.title}>
-                    <Text id="app.navigation.tabs.friends" />
-                </div>
-                <div className={styles.actions}>
-                    {/*<Tooltip content={"Create Category"} placement="bottom">
+	const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
+	return (
+		<>
+			<Header placement="primary">
+				{!isTouchscreenDevice && <UserDetail size={24} />}
+				<div className={styles.title}>
+					<Text id="app.navigation.tabs.friends" />
+				</div>
+				<div className={styles.actions}>
+					{/*<Tooltip content={"Create Category"} placement="bottom">
                         <IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_group' })}>
                             <ListPlus size={28} />
                         </IconButton>
                     </Tooltip>
                     <div className={styles.divider} />*/}
-                    <Tooltip content={"Create Group"} placement="bottom">
-                        <IconButton onClick={() => openScreen({ id: 'special_input', type: 'create_group' })}>
-                            <MessageAdd size={24} />
-                        </IconButton>
-                    </Tooltip>
-                    <Tooltip content={"Add Friend"} placement="bottom">
-                        <IconButton onClick={() => openScreen({ id: 'special_input', type: 'add_friend' })}>
-                            <UserPlus size={27} />
-                        </IconButton>
-                    </Tooltip>
-                    {/* 
+					<Tooltip content={"Create Group"} placement="bottom">
+						<IconButton
+							onClick={() =>
+								openScreen({
+									id: "special_input",
+									type: "create_group",
+								})
+							}>
+							<MessageAdd size={24} />
+						</IconButton>
+					</Tooltip>
+					<Tooltip content={"Add Friend"} placement="bottom">
+						<IconButton
+							onClick={() =>
+								openScreen({
+									id: "special_input",
+									type: "add_friend",
+								})
+							}>
+							<UserPlus size={27} />
+						</IconButton>
+					</Tooltip>
+					{/* 
                     <div className={styles.divider} />
                     <Tooltip content={"Friend Activity"} placement="bottom">
                         <IconButton>
@@ -80,51 +119,98 @@ export default function Friends() {
                         </IconButton>            
                     </Tooltip>
                     */}
-                </div>
-            </Header>
-            <div className={styles.list} data-empty={isEmpty}>
-                {isEmpty && (
-                    <>
-                        <img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
-                        <Text id="app.special.friends.nobody" />
-                    </>
-                )}
+				</div>
+			</Header>
+			<div className={styles.list} data-empty={isEmpty}>
+				{isEmpty && (
+					<>
+						<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
+						<Text id="app.special.friends.nobody" />
+					</>
+				)}
 
-                { incoming.length > 0 && <div className={styles.pending}
-                    onClick={() => openScreen({ id: 'pending_requests', users: incoming.map(x => x._id) })}>
-                    <div className={styles.avatars}>
-                        { incoming.map((x, i) => i < 3 && <UserIcon target={x} size={64} mask={ i < Math.min(incoming.length - 1, 2) ? "url(#overlap)" : undefined } />) }
-                    </div>
-                    <div className={styles.details}>
-                        <div><Text id="app.special.friends.pending" /> <span>{ incoming.length }</span></div>
-                        <span>
-                            {
-                                  incoming.length > 3 ? <TextReact id="app.special.friends.from.several" fields={{ userlist: userlist.slice(0, 6), count: incoming.length - 3 }} />
-                                : incoming.length > 1 ? <TextReact id="app.special.friends.from.multiple" fields={{ user: userlist.shift()!, userlist: userlist.slice(1) }} />
-                                : <TextReact id="app.special.friends.from.single" fields={{ user: userlist[0] }} />
-                            }
-                        </span>
-                    </div>
-                    <ChevronRight size={28} />
-                </div> }
+				{incoming.length > 0 && (
+					<div
+						className={styles.pending}
+						onClick={() =>
+							openScreen({
+								id: "pending_requests",
+								users: incoming.map((x) => x._id),
+							})
+						}>
+						<div className={styles.avatars}>
+							{incoming.map(
+								(x, i) =>
+									i < 3 && (
+										<UserIcon
+											target={x}
+											size={64}
+											mask={
+												i <
+												Math.min(incoming.length - 1, 2)
+													? "url(#overlap)"
+													: undefined
+											}
+										/>
+									),
+							)}
+						</div>
+						<div className={styles.details}>
+							<div>
+								<Text id="app.special.friends.pending" />{" "}
+								<span>{incoming.length}</span>
+							</div>
+							<span>
+								{incoming.length > 3 ? (
+									<TextReact
+										id="app.special.friends.from.several"
+										fields={{
+											userlist: userlist.slice(0, 6),
+											count: incoming.length - 3,
+										}}
+									/>
+								) : incoming.length > 1 ? (
+									<TextReact
+										id="app.special.friends.from.multiple"
+										fields={{
+											user: userlist.shift()!,
+											userlist: userlist.slice(1),
+										}}
+									/>
+								) : (
+									<TextReact
+										id="app.special.friends.from.single"
+										fields={{ user: userlist[0] }}
+									/>
+								)}
+							</span>
+						</div>
+						<ChevronRight size={28} />
+					</div>
+				)}
 
-                {
-                    lists.map(([i18n, list, section_id], index) => {
-                        if (index === 0) return;
-                        if (list.length === 0) return;
+				{lists.map(([i18n, list, section_id], index) => {
+					if (index === 0) return;
+					if (list.length === 0) return;
 
-                        return (
-                            <CollapsibleSection
-                                id={`friends_${section_id}`}
-                                defaultValue={true}
-                                sticky large
-                                summary={<div class="title"><Text id={i18n} /> — { list.length }</div>}>
-                                { list.map(x => <Friend key={x._id} user={x} />) }
-                            </CollapsibleSection>
-                        )
-                    })
-                }
-            </div>
-        </>
-    );
+					return (
+						<CollapsibleSection
+							id={`friends_${section_id}`}
+							defaultValue={true}
+							sticky
+							large
+							summary={
+								<div class="title">
+									<Text id={i18n} /> — {list.length}
+								</div>
+							}>
+							{list.map((x) => (
+								<Friend key={x._id} user={x} />
+							))}
+						</CollapsibleSection>
+					);
+				})}
+			</div>
+		</>
+	);
 }
diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx
index d45e15b57c584c04f5c36350bbb03027c602ed08..50f3c4941fbfb2efd81a2b94531be8b65f4ee6e8 100644
--- a/src/pages/home/Home.tsx
+++ b/src/pages/home/Home.tsx
@@ -1,39 +1,41 @@
-import styles from "./Home.module.scss";
+import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
 import { Link } from "react-router-dom";
 
+import styles from "./Home.module.scss";
 import { Text } from "preact-i18n";
+
+import wideSVG from "../../assets/wide.svg";
 import Header from "../../components/ui/Header";
-import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
-import wideSVG from '../../assets/wide.svg';
 
 export default function Home() {
-    return (
-        <div className={styles.home}>
-            <Header placement="primary">
-                <HomeIcon size={24} />
-                <Text id="app.navigation.tabs.home" />
-            </Header>
-            <h3>
-                <Text id="app.special.modals.onboarding.welcome" /> <img src={wideSVG} />
-            </h3>
-            <ul>
-                <li>
-                    Go to your <Link to="/friends">friends list</Link>.
-                </li>
-                <li>
-                    Give <Link to="/settings/feedback">feedback</Link>.
-                </li>
-                <li>
-                    Join <Link to="/invite/Testers">testers server</Link>.
-                </li>
-                <li>
-                    View{" "}
-                    <a href="https://gitlab.insrt.uk/revolt" target="_blank">
-                        source code
-                    </a>
-                    .
-                </li>
-            </ul>
-        </div>
-    );
+	return (
+		<div className={styles.home}>
+			<Header placement="primary">
+				<HomeIcon size={24} />
+				<Text id="app.navigation.tabs.home" />
+			</Header>
+			<h3>
+				<Text id="app.special.modals.onboarding.welcome" />{" "}
+				<img src={wideSVG} />
+			</h3>
+			<ul>
+				<li>
+					Go to your <Link to="/friends">friends list</Link>.
+				</li>
+				<li>
+					Give <Link to="/settings/feedback">feedback</Link>.
+				</li>
+				<li>
+					Join <Link to="/invite/Testers">testers server</Link>.
+				</li>
+				<li>
+					View{" "}
+					<a href="https://gitlab.insrt.uk/revolt" target="_blank">
+						source code
+					</a>
+					.
+				</li>
+			</ul>
+		</div>
+	);
 }
diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx
index e05bcdf3f9f27a369b2e76c67dd02762c0858524..eae15f08290f38e71b401d6cc00ea91687f3a22e 100644
--- a/src/pages/invite/Invite.tsx
+++ b/src/pages/invite/Invite.tsx
@@ -1,88 +1,128 @@
-import styles from './Invite.module.scss';
-import Button from '../../components/ui/Button';
 import { ArrowBack } from "@styled-icons/boxicons-regular";
-import Overline from '../../components/ui/Overline';
-import { Invites } from "revolt.js/dist/api/objects";
-import Preloader from '../../components/ui/Preloader';
-import { takeError } from "../../context/revoltjs/util";
 import { useHistory, useParams } from "react-router-dom";
-import ServerIcon from '../../components/common/ServerIcon';
-import UserIcon from '../../components/common/user/UserIcon';
+import { Invites } from "revolt.js/dist/api/objects";
+
+import styles from "./Invite.module.scss";
 import { useContext, useEffect, useState } from "preact/hooks";
-import RequiresOnline from '../../context/revoltjs/RequiresOnline';
-import { AppContext, ClientStatus, StatusContext } from "../../context/revoltjs/RevoltClient";
+
+import RequiresOnline from "../../context/revoltjs/RequiresOnline";
+import {
+	AppContext,
+	ClientStatus,
+	StatusContext,
+} from "../../context/revoltjs/RevoltClient";
+import { takeError } from "../../context/revoltjs/util";
+
+import ServerIcon from "../../components/common/ServerIcon";
+import UserIcon from "../../components/common/user/UserIcon";
+import Button from "../../components/ui/Button";
+import Overline from "../../components/ui/Overline";
+import Preloader from "../../components/ui/Preloader";
 
 export default function Invite() {
-    const history = useHistory();
-    const client = useContext(AppContext);
-    const status = useContext(StatusContext);
-    const { code } = useParams<{ code: string }>();
-    const [ processing, setProcessing ] = useState(false);
-    const [ error, setError ] = useState<string | undefined>(undefined);
-    const [ invite, setInvite ] = useState<Invites.RetrievedInvite | undefined>(undefined);
+	const history = useHistory();
+	const client = useContext(AppContext);
+	const status = useContext(StatusContext);
+	const { code } = useParams<{ code: string }>();
+	const [processing, setProcessing] = useState(false);
+	const [error, setError] = useState<string | undefined>(undefined);
+	const [invite, setInvite] = useState<Invites.RetrievedInvite | undefined>(
+		undefined,
+	);
 
-    useEffect(() => {
-        if (typeof invite === 'undefined' && (status === ClientStatus.ONLINE || status === ClientStatus.READY)) {
-            client.fetchInvite(code)
-                .then(data => setInvite(data))
-                .catch(err => setError(takeError(err)))
-        }
-    }, [ status ]);
+	useEffect(() => {
+		if (
+			typeof invite === "undefined" &&
+			(status === ClientStatus.ONLINE || status === ClientStatus.READY)
+		) {
+			client
+				.fetchInvite(code)
+				.then((data) => setInvite(data))
+				.catch((err) => setError(takeError(err)));
+		}
+	}, [status]);
 
-    if (typeof invite === 'undefined') {
-        return (
-            <div className={styles.preloader}>
-                <RequiresOnline>
-                    { error ? <Overline type="error" error={error} />
-                        : <Preloader type="spinner" /> }
-                </RequiresOnline>
-            </div>
-        )
-    }
+	if (typeof invite === "undefined") {
+		return (
+			<div className={styles.preloader}>
+				<RequiresOnline>
+					{error ? (
+						<Overline type="error" error={error} />
+					) : (
+						<Preloader type="spinner" />
+					)}
+				</RequiresOnline>
+			</div>
+		);
+	}
 
-    // ! FIXME: add i18n translations
-    return (
-        <div className={styles.invite} style={{ backgroundImage: invite.server_banner ? `url('${client.generateFileURL(invite.server_banner)}')` : undefined }}>
-            <div className={styles.leave}>
-                <ArrowBack size={32} onClick={() => history.push('/')} />
-            </div>
+	// ! FIXME: add i18n translations
+	return (
+		<div
+			className={styles.invite}
+			style={{
+				backgroundImage: invite.server_banner
+					? `url('${client.generateFileURL(invite.server_banner)}')`
+					: undefined,
+			}}>
+			<div className={styles.leave}>
+				<ArrowBack size={32} onClick={() => history.push("/")} />
+			</div>
 
-            { !processing && 
-                <div className={styles.icon}>
-                    <ServerIcon attachment={invite.server_icon} server_name={invite.server_name} size={64} />
-                </div> }
+			{!processing && (
+				<div className={styles.icon}>
+					<ServerIcon
+						attachment={invite.server_icon}
+						server_name={invite.server_name}
+						size={64}
+					/>
+				</div>
+			)}
 
-            <div className={styles.details}>
-                { processing ? <Preloader type="ring" /> :
-                    <>
-                        <h1>{ invite.server_name }</h1>
-                        <h2>#{invite.channel_name}</h2>
-                        <h3>Invited by <UserIcon size={24} attachment={invite.user_avatar} /> { invite.user_name }</h3>
-                        <Overline type="error" error={error} />
-                        <Button contrast
-                            onClick={
-                                async () => {
-                                    if (status === ClientStatus.READY) {
-                                        return history.push('/');
-                                    }
+			<div className={styles.details}>
+				{processing ? (
+					<Preloader type="ring" />
+				) : (
+					<>
+						<h1>{invite.server_name}</h1>
+						<h2>#{invite.channel_name}</h2>
+						<h3>
+							Invited by{" "}
+							<UserIcon
+								size={24}
+								attachment={invite.user_avatar}
+							/>{" "}
+							{invite.user_name}
+						</h3>
+						<Overline type="error" error={error} />
+						<Button
+							contrast
+							onClick={async () => {
+								if (status === ClientStatus.READY) {
+									return history.push("/");
+								}
 
-                                    try {
-                                        setProcessing(true);
+								try {
+									setProcessing(true);
 
-                                        let result = await client.joinInvite(code);
-                                        if (result.type === 'Server') {
-                                            history.push(`/server/${result.server._id}/channel/${result.channel._id}`);
-                                        }
-                                    } catch (err) {
-                                        setError(takeError(err));
-                                        setProcessing(false);
-                                    }
-                                }
-                            }
-                        >{ status === ClientStatus.READY ? 'Login to REVOLT' : 'Accept Invite' }</Button>
-                    </>
-                }
-            </div>
-        </div>
-    );
+									let result = await client.joinInvite(code);
+									if (result.type === "Server") {
+										history.push(
+											`/server/${result.server._id}/channel/${result.channel._id}`,
+										);
+									}
+								} catch (err) {
+									setError(takeError(err));
+									setProcessing(false);
+								}
+							}}>
+							{status === ClientStatus.READY
+								? "Login to REVOLT"
+								: "Accept Invite"}
+						</Button>
+					</>
+				)}
+			</div>
+		</div>
+	);
 }
diff --git a/src/pages/login/FormField.tsx b/src/pages/login/FormField.tsx
index 94a36098d7bda22f459b56d4d2bf3da804f1067b..059e6be8249c385d44435c7cf61ef9803fa8f818 100644
--- a/src/pages/login/FormField.tsx
+++ b/src/pages/login/FormField.tsx
@@ -1,70 +1,71 @@
-import Overline from '../../components/ui/Overline';
-import InputBox from '../../components/ui/InputBox';
-import { Text, Localizer } from 'preact-i18n';
+import { Text, Localizer } from "preact-i18n";
+
+import InputBox from "../../components/ui/InputBox";
+import Overline from "../../components/ui/Overline";
 
 interface Props {
-    type: "email" | "username" | "password" | "invite" | "current_password";
-    showOverline?: boolean;
-    register: Function;
-    error?: string;
-    name?: string;
+	type: "email" | "username" | "password" | "invite" | "current_password";
+	showOverline?: boolean;
+	register: Function;
+	error?: string;
+	name?: string;
 }
 
 export default function FormField({
-    type,
-    register,
-    showOverline,
-    error,
-    name
+	type,
+	register,
+	showOverline,
+	error,
+	name,
 }: Props) {
-    return (
-        <>
-            {showOverline && (
-                <Overline error={error}>
-                    <Text id={`login.${type}`} />
-                </Overline>
-            )}
-            <Localizer>
-                <InputBox
-                    // Styled uses React typing while we use Preact
-                    // this leads to inconsistances where things need to be typed oddly
-                    placeholder={(<Text id={`login.enter.${type}`} />) as any}
-                    name={
-                        type === "current_password" ? "password" : name ?? type
-                    }
-                    type={
-                        type === "invite" || type === "username"
-                            ? "text"
-                            : type === "current_password"
-                            ? "password"
-                            : type
-                    }
-                    ref={register(
-                        type === "password" || type === "current_password"
-                            ? {
-                                  validate: (value: string) =>
-                                      value.length === 0
-                                          ? "RequiredField"
-                                          : value.length < 8
-                                          ? "TooShort"
-                                          : value.length > 1024
-                                          ? "TooLong"
-                                          : undefined
-                              }
-                            : type === "email"
-                            ? {
-                                  required: "RequiredField",
-                                  pattern: {
-                                      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
-                                      message: "InvalidEmail"
-                                  }
-                              }
-                            : type === "username"
-                            ? { required: "RequiredField" }
-                            : { required: "RequiredField" }
-                    )}
-                />
-            </Localizer>
-        </>
-    );
+	return (
+		<>
+			{showOverline && (
+				<Overline error={error}>
+					<Text id={`login.${type}`} />
+				</Overline>
+			)}
+			<Localizer>
+				<InputBox
+					// Styled uses React typing while we use Preact
+					// this leads to inconsistances where things need to be typed oddly
+					placeholder={(<Text id={`login.enter.${type}`} />) as any}
+					name={
+						type === "current_password" ? "password" : name ?? type
+					}
+					type={
+						type === "invite" || type === "username"
+							? "text"
+							: type === "current_password"
+							? "password"
+							: type
+					}
+					ref={register(
+						type === "password" || type === "current_password"
+							? {
+									validate: (value: string) =>
+										value.length === 0
+											? "RequiredField"
+											: value.length < 8
+											? "TooShort"
+											: value.length > 1024
+											? "TooLong"
+											: undefined,
+							  }
+							: type === "email"
+							? {
+									required: "RequiredField",
+									pattern: {
+										value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
+										message: "InvalidEmail",
+									},
+							  }
+							: type === "username"
+							? { required: "RequiredField" }
+							: { required: "RequiredField" },
+					)}
+				/>
+			</Localizer>
+		</>
+	);
 }
diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx
index e24f333e74367cecc0afec309789e704fea59ff9..ffc2e2cd5da5c1e59d7e15ebc7da84cae6d51694 100644
--- a/src/pages/login/Login.tsx
+++ b/src/pages/login/Login.tsx
@@ -1,72 +1,74 @@
-import { Text } from "preact-i18n";
 import { Helmet } from "react-helmet";
+import { Route, Switch } from "react-router-dom";
+import { LIBRARY_VERSION } from "revolt.js";
+
 import styles from "./Login.module.scss";
+import { Text } from "preact-i18n";
 import { useContext } from "preact/hooks";
-import { APP_VERSION } from "../../version";
-import { LIBRARY_VERSION } from "revolt.js";
-import { Route, Switch } from "react-router-dom";
+
 import { ThemeContext } from "../../context/Theme";
 import { AppContext } from "../../context/revoltjs/RevoltClient";
+
 import LocaleSelector from "../../components/common/LocaleSelector";
 
+import { APP_VERSION } from "../../version";
 import background from "./background.jpg";
-
-import { FormLogin } from "./forms/FormLogin";
 import { FormCreate } from "./forms/FormCreate";
+import { FormLogin } from "./forms/FormLogin";
 import { FormResend } from "./forms/FormResend";
 import { FormReset, FormSendReset } from "./forms/FormReset";
 
 export default function Login() {
-    const theme = useContext(ThemeContext);
-    const client = useContext(AppContext);
+	const theme = useContext(ThemeContext);
+	const client = useContext(AppContext);
 
-    return (
-        <div className={styles.login}>
-            <Helmet>
-                <meta name="theme-color" content={theme.background} />
-            </Helmet>
-            <div className={styles.content}>
-                <div className={styles.attribution}>
-                    <span>
-                        API:{" "}
-                        <code>{client.configuration?.revolt ?? "???"}</code>{" "}
-                        &middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
-                        &middot; App: <code>{APP_VERSION}</code>
-                    </span>
-                    <span>
-                        <LocaleSelector />
-                    </span>
-                </div>
-                <div className={styles.modal}>
-                    <Switch>
-                        <Route path="/login/create">
-                            <FormCreate />
-                        </Route>
-                        <Route path="/login/resend">
-                            <FormResend />
-                        </Route>
-                        <Route path="/login/reset/:token">
-                            <FormReset />
-                        </Route>
-                        <Route path="/login/reset">
-                            <FormSendReset />
-                        </Route>
-                        <Route path="/">
-                            <FormLogin />
-                        </Route>
-                    </Switch>
-                </div>
-                <div className={styles.attribution}>
-                    <span>
-                        <Text id="general.image_by" /> &lrm;@lorenzoherrera
-                        &rlm;· unsplash.com
-                    </span>
-                </div>
-            </div>
-            <div
-                className={styles.bg}
-                style={{ background: `url('${background}')` }}
-            />
-        </div>
-    );
-};
+	return (
+		<div className={styles.login}>
+			<Helmet>
+				<meta name="theme-color" content={theme.background} />
+			</Helmet>
+			<div className={styles.content}>
+				<div className={styles.attribution}>
+					<span>
+						API:{" "}
+						<code>{client.configuration?.revolt ?? "???"}</code>{" "}
+						&middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
+						&middot; App: <code>{APP_VERSION}</code>
+					</span>
+					<span>
+						<LocaleSelector />
+					</span>
+				</div>
+				<div className={styles.modal}>
+					<Switch>
+						<Route path="/login/create">
+							<FormCreate />
+						</Route>
+						<Route path="/login/resend">
+							<FormResend />
+						</Route>
+						<Route path="/login/reset/:token">
+							<FormReset />
+						</Route>
+						<Route path="/login/reset">
+							<FormSendReset />
+						</Route>
+						<Route path="/">
+							<FormLogin />
+						</Route>
+					</Switch>
+				</div>
+				<div className={styles.attribution}>
+					<span>
+						<Text id="general.image_by" /> &lrm;@lorenzoherrera
+						&rlm;· unsplash.com
+					</span>
+				</div>
+			</div>
+			<div
+				className={styles.bg}
+				style={{ background: `url('${background}')` }}
+			/>
+		</div>
+	);
+}
diff --git a/src/pages/login/forms/CaptchaBlock.tsx b/src/pages/login/forms/CaptchaBlock.tsx
index f435f017e8763dbb0f6a1f046cd7459ef538059b..9cca755ef72ae71f17b0d032e285aa75f2c41831 100644
--- a/src/pages/login/forms/CaptchaBlock.tsx
+++ b/src/pages/login/forms/CaptchaBlock.tsx
@@ -1,38 +1,41 @@
-import { Text } from "preact-i18n";
-import styles from "../Login.module.scss";
 import HCaptcha from "@hcaptcha/react-hcaptcha";
+
+import styles from "../Login.module.scss";
+import { Text } from "preact-i18n";
 import { useContext, useEffect } from "preact/hooks";
-import Preloader from "../../../components/ui/Preloader";
+
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 
+import Preloader from "../../../components/ui/Preloader";
+
 export interface CaptchaProps {
-    onSuccess: (token?: string) => void;
-    onCancel: () => void;
+	onSuccess: (token?: string) => void;
+	onCancel: () => void;
 }
 
 export function CaptchaBlock(props: CaptchaProps) {
-    const client = useContext(AppContext);
-
-    useEffect(() => {
-        if (!client.configuration?.features.captcha.enabled) {
-            props.onSuccess();
-        }
-    }, []);
-
-    if (!client.configuration?.features.captcha.enabled)
-        return <Preloader type="spinner" />;
-
-    return (
-        <div>
-            <HCaptcha
-                sitekey={client.configuration.features.captcha.key}
-                onVerify={token => props.onSuccess(token)}
-            />
-            <div className={styles.footer}>
-                <a onClick={props.onCancel}>
-                    <Text id="login.cancel" />
-                </a>
-            </div>
-        </div>
-    );
+	const client = useContext(AppContext);
+
+	useEffect(() => {
+		if (!client.configuration?.features.captcha.enabled) {
+			props.onSuccess();
+		}
+	}, []);
+
+	if (!client.configuration?.features.captcha.enabled)
+		return <Preloader type="spinner" />;
+
+	return (
+		<div>
+			<HCaptcha
+				sitekey={client.configuration.features.captcha.key}
+				onVerify={(token) => props.onSuccess(token)}
+			/>
+			<div className={styles.footer}>
+				<a onClick={props.onCancel}>
+					<Text id="login.cancel" />
+				</a>
+			</div>
+		</div>
+	);
 }
diff --git a/src/pages/login/forms/Form.tsx b/src/pages/login/forms/Form.tsx
index 66791ec3529280d13ae99278b1e1b0331b1f00c7..4caf7b9eee43fdce629f269a068a6e41e16028a2 100644
--- a/src/pages/login/forms/Form.tsx
+++ b/src/pages/login/forms/Form.tsx
@@ -1,244 +1,251 @@
-import { Legal } from "./Legal";
-import { Text } from "preact-i18n";
+import { CheckCircle, Envelope } from "@styled-icons/boxicons-regular";
+import { useForm } from "react-hook-form";
 import { Link } from "react-router-dom";
+
 import styles from "../Login.module.scss";
-import { useForm } from "react-hook-form";
-import { MailProvider } from "./MailProvider";
+import { Text } from "preact-i18n";
 import { useContext, useState } from "preact/hooks";
-import { CheckCircle, Envelope } from "@styled-icons/boxicons-regular";
-import { takeError } from "../../../context/revoltjs/util";
-import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
+
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import { takeError } from "../../../context/revoltjs/util";
 
-import FormField from "../FormField";
+import wideSVG from "../../../assets/wide.svg";
 import Button from "../../../components/ui/Button";
 import Overline from "../../../components/ui/Overline";
 import Preloader from "../../../components/ui/Preloader";
 
-import wideSVG from '../../../assets/wide.svg';
+import FormField from "../FormField";
+import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
+import { Legal } from "./Legal";
+import { MailProvider } from "./MailProvider";
 
 interface Props {
-    page: "create" | "login" | "send_reset" | "reset" | "resend";
-    callback: (fields: {
-        email: string;
-        password: string;
-        invite: string;
-        captcha?: string;
-    }) => Promise<void>;
+	page: "create" | "login" | "send_reset" | "reset" | "resend";
+	callback: (fields: {
+		email: string;
+		password: string;
+		invite: string;
+		captcha?: string;
+	}) => Promise<void>;
 }
 
 function getInviteCode() {
-    if (typeof window === 'undefined') return '';
+	if (typeof window === "undefined") return "";
 
-    const urlParams = new URLSearchParams(window.location.search);
-    const code = urlParams.get('code');
-    return code ?? '';
+	const urlParams = new URLSearchParams(window.location.search);
+	const code = urlParams.get("code");
+	return code ?? "";
 }
 
 interface FormInputs {
-    email: string
-    password: string
-    invite: string
+	email: string;
+	password: string;
+	invite: string;
 }
 
 export function Form({ page, callback }: Props) {
-    const client = useContext(AppContext);
-
-    const [loading, setLoading] = useState(false);
-    const [success, setSuccess] = useState<string | undefined>(undefined);
-    const [error, setGlobalError] = useState<string | undefined>(undefined);
-    const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
-
-    const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
-        defaultValues: {
-            email: '',
-            password: '',
-            invite: getInviteCode()
-        }
-    });
-
-    async function onSubmit(data: FormInputs) {
-        setGlobalError(undefined);
-        setLoading(true);
-
-        function onError(err: any) {
-            setLoading(false);
-
-            const error = takeError(err);
-            switch (error) {
-                case "email_in_use":
-                    return setError("email", { type: "", message: error });
-                case "unknown_user":
-                    return setError("email", { type: "", message: error });
-                case "invalid_invite":
-                    return setError("invite", { type: "", message: error });
-            }
-
-            setGlobalError(error);
-        }
-
-        try {
-            if (
-                client.configuration?.features.captcha.enabled &&
-                page !== "reset"
-            ) {
-                setCaptcha({
-                    onSuccess: async captcha => {
-                        setCaptcha(undefined);
-                        try {
-                            await callback({ ...data, captcha });
-                            setSuccess(data.email);
-                        } catch (err) {
-                            onError(err);
-                        }
-                    },
-                    onCancel: () => {
-                        setCaptcha(undefined);
-                        setLoading(false);
-                    }
-                });
-            } else {
-                await callback(data);
-                setSuccess(data.email);
-            }
-        } catch (err) {
-            onError(err);
-        }
-    }
-
-    if (typeof success !== "undefined") {
-        return (
-            <div className={styles.success}>
-                {client.configuration?.features.email ? (
-                    <>
-                        <Envelope size={72} />
-                        <h2>
-                            <Text id="login.check_mail" />
-                        </h2>
-                        <p className={styles.note}>
-                            <Text id="login.email_delay" />
-                        </p>
-                        <MailProvider email={success} />
-                    </>
-                ) : (
-                    <>
-                        <CheckCircle size={72} />
-                        <h1>
-                            <Text id="login.successful_registration" />
-                        </h1>
-                    </>
-                )}
-                <span className={styles.footer}>
-                    <Link to="/login">
-                        <a>
-                            <Text id="login.remembered" />
-                        </a>
-                    </Link>
-                </span>
-            </div>
-        );
-    }
-
-    if (captcha) return <CaptchaBlock {...captcha} />;
-    if (loading) return <Preloader type="spinner" />;
-
-    return (
-        <div className={styles.form}>
-            <img src={wideSVG} />
-            {/* Preact / React typing incompatabilities */}
-            <form onSubmit={handleSubmit(onSubmit) as JSX.GenericEventHandler<HTMLFormElement>}>
-                {page !== "reset" && (
-                    <FormField
-                        type="email"
-                        register={register}
-                        showOverline
-                        error={errors.email?.message}
-                    />
-                )}
-                {(page === "login" ||
-                    page === "create" ||
-                    page === "reset") && (
-                    <FormField
-                        type="password"
-                        register={register}
-                        showOverline
-                        error={errors.password?.message}
-                    />
-                )}
-                {client.configuration?.features.invite_only &&
-                    page === "create" && (
-                        <FormField
-                            type="invite"
-                            register={register}
-                            showOverline
-                            error={errors.invite?.message}
-                        />
-                    )}
-                {error && (
-                    <Overline type="error" error={error}>
-                        <Text id={`login.error.${page}`} />
-                    </Overline>
-                )}
-                <Button>
-                    <Text
-                        id={
-                            page === "create"
-                                ? "login.register"
-                                : page === "login"
-                                ? "login.title"
-                                : page === "reset"
-                                ? "login.set_password"
-                                : page === "resend"
-                                ? "login.resend"
-                                : "login.reset"
-                        }
-                    />
-                </Button>
-            </form>
-            {page === "create" && (
-                <>
-                    <span className={styles.create}>
-                        <Text id="login.existing" />
-                        <Link to="/login">
-                            <Text id="login.title" />
-                        </Link>
-                    </span>
-                    <span className={styles.create}>
-                        <Text id="login.missing_verification" />
-                        <Link to="/login/resend">
-                            <Text id="login.resend" />
-                        </Link>
-                    </span>
-                </>
-            )}
-            {page === "login" && (
-                <>
-                    <span className={styles.create}>
-                        <Text id="login.new" />
-                        <Link to="/login/create">
-                            <Text id="login.create" />
-                        </Link>
-                    </span>
-                    <span className={styles.create}>
-                        <Text id="login.forgot" />
-                        <Link to="/login/reset">
-                            <Text id="login.reset" />
-                        </Link>
-                    </span>
-                </>
-            )}
-            {(page === "reset" ||
-                page === "resend" ||
-                page === "send_reset") && (
-                <>
-                    <span className={styles.create}>
-                        <Link to="/login">
-                            <Text id="login.remembered" />
-                        </Link>
-                    </span>
-                </>
-            )}
-            <Legal />
-        </div>
-    );
+	const client = useContext(AppContext);
+
+	const [loading, setLoading] = useState(false);
+	const [success, setSuccess] = useState<string | undefined>(undefined);
+	const [error, setGlobalError] = useState<string | undefined>(undefined);
+	const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
+
+	const { handleSubmit, register, errors, setError } = useForm<FormInputs>({
+		defaultValues: {
+			email: "",
+			password: "",
+			invite: getInviteCode(),
+		},
+	});
+
+	async function onSubmit(data: FormInputs) {
+		setGlobalError(undefined);
+		setLoading(true);
+
+		function onError(err: any) {
+			setLoading(false);
+
+			const error = takeError(err);
+			switch (error) {
+				case "email_in_use":
+					return setError("email", { type: "", message: error });
+				case "unknown_user":
+					return setError("email", { type: "", message: error });
+				case "invalid_invite":
+					return setError("invite", { type: "", message: error });
+			}
+
+			setGlobalError(error);
+		}
+
+		try {
+			if (
+				client.configuration?.features.captcha.enabled &&
+				page !== "reset"
+			) {
+				setCaptcha({
+					onSuccess: async (captcha) => {
+						setCaptcha(undefined);
+						try {
+							await callback({ ...data, captcha });
+							setSuccess(data.email);
+						} catch (err) {
+							onError(err);
+						}
+					},
+					onCancel: () => {
+						setCaptcha(undefined);
+						setLoading(false);
+					},
+				});
+			} else {
+				await callback(data);
+				setSuccess(data.email);
+			}
+		} catch (err) {
+			onError(err);
+		}
+	}
+
+	if (typeof success !== "undefined") {
+		return (
+			<div className={styles.success}>
+				{client.configuration?.features.email ? (
+					<>
+						<Envelope size={72} />
+						<h2>
+							<Text id="login.check_mail" />
+						</h2>
+						<p className={styles.note}>
+							<Text id="login.email_delay" />
+						</p>
+						<MailProvider email={success} />
+					</>
+				) : (
+					<>
+						<CheckCircle size={72} />
+						<h1>
+							<Text id="login.successful_registration" />
+						</h1>
+					</>
+				)}
+				<span className={styles.footer}>
+					<Link to="/login">
+						<a>
+							<Text id="login.remembered" />
+						</a>
+					</Link>
+				</span>
+			</div>
+		);
+	}
+
+	if (captcha) return <CaptchaBlock {...captcha} />;
+	if (loading) return <Preloader type="spinner" />;
+
+	return (
+		<div className={styles.form}>
+			<img src={wideSVG} />
+			{/* Preact / React typing incompatabilities */}
+			<form
+				onSubmit={
+					handleSubmit(
+						onSubmit,
+					) as JSX.GenericEventHandler<HTMLFormElement>
+				}>
+				{page !== "reset" && (
+					<FormField
+						type="email"
+						register={register}
+						showOverline
+						error={errors.email?.message}
+					/>
+				)}
+				{(page === "login" ||
+					page === "create" ||
+					page === "reset") && (
+					<FormField
+						type="password"
+						register={register}
+						showOverline
+						error={errors.password?.message}
+					/>
+				)}
+				{client.configuration?.features.invite_only &&
+					page === "create" && (
+						<FormField
+							type="invite"
+							register={register}
+							showOverline
+							error={errors.invite?.message}
+						/>
+					)}
+				{error && (
+					<Overline type="error" error={error}>
+						<Text id={`login.error.${page}`} />
+					</Overline>
+				)}
+				<Button>
+					<Text
+						id={
+							page === "create"
+								? "login.register"
+								: page === "login"
+								? "login.title"
+								: page === "reset"
+								? "login.set_password"
+								: page === "resend"
+								? "login.resend"
+								: "login.reset"
+						}
+					/>
+				</Button>
+			</form>
+			{page === "create" && (
+				<>
+					<span className={styles.create}>
+						<Text id="login.existing" />
+						<Link to="/login">
+							<Text id="login.title" />
+						</Link>
+					</span>
+					<span className={styles.create}>
+						<Text id="login.missing_verification" />
+						<Link to="/login/resend">
+							<Text id="login.resend" />
+						</Link>
+					</span>
+				</>
+			)}
+			{page === "login" && (
+				<>
+					<span className={styles.create}>
+						<Text id="login.new" />
+						<Link to="/login/create">
+							<Text id="login.create" />
+						</Link>
+					</span>
+					<span className={styles.create}>
+						<Text id="login.forgot" />
+						<Link to="/login/reset">
+							<Text id="login.reset" />
+						</Link>
+					</span>
+				</>
+			)}
+			{(page === "reset" ||
+				page === "resend" ||
+				page === "send_reset") && (
+				<>
+					<span className={styles.create}>
+						<Link to="/login">
+							<Text id="login.remembered" />
+						</Link>
+					</span>
+				</>
+			)}
+			<Legal />
+		</div>
+	);
 }
diff --git a/src/pages/login/forms/FormCreate.tsx b/src/pages/login/forms/FormCreate.tsx
index 4336acd2e92b2d2aac2ae707788df421c584933b..2ea6e323eacd7e0f6dd19a5a5dda8a231ac4b3b3 100644
--- a/src/pages/login/forms/FormCreate.tsx
+++ b/src/pages/login/forms/FormCreate.tsx
@@ -1,16 +1,18 @@
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
 import { useContext } from "preact/hooks";
+
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
 import { Form } from "./Form";
 
 export function FormCreate() {
-    const client = useContext(AppContext);
+	const client = useContext(AppContext);
 
-    return (
-        <Form
-            page="create"
-            callback={async data => {
-                await client.register(import.meta.env.VITE_API_URL, data);
-            }}
-        />
-    );
+	return (
+		<Form
+			page="create"
+			callback={async (data) => {
+				await client.register(import.meta.env.VITE_API_URL, data);
+			}}
+		/>
+	);
 }
diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx
index d3af8a18f1250ed404c1edcb7e512f32e1e5bd98..b7ea421ad0f6e216987da266a261faf0a626c3d2 100644
--- a/src/pages/login/forms/FormLogin.tsx
+++ b/src/pages/login/forms/FormLogin.tsx
@@ -1,29 +1,32 @@
-import { Form } from "./Form";
 import { detect } from "detect-browser";
-import { useContext } from "preact/hooks";
 import { useHistory } from "react-router-dom";
+
+import { useContext } from "preact/hooks";
+
 import { OperationsContext } from "../../../context/revoltjs/RevoltClient";
 
+import { Form } from "./Form";
+
 export function FormLogin() {
-    const { login } = useContext(OperationsContext);
-    const history = useHistory();
+	const { login } = useContext(OperationsContext);
+	const history = useHistory();
 
-    return (
-        <Form
-            page="login"
-            callback={async data => {
-                const browser = detect();
-                let device_name;
-                if (browser) {
-                    const { name, os } = browser;
-                    device_name = `${name} on ${os}`;
-                } else {
-                    device_name = "Unknown Device";
-                }
+	return (
+		<Form
+			page="login"
+			callback={async (data) => {
+				const browser = detect();
+				let device_name;
+				if (browser) {
+					const { name, os } = browser;
+					device_name = `${name} on ${os}`;
+				} else {
+					device_name = "Unknown Device";
+				}
 
-                await login({ ...data, device_name });
-                history.push("/");
-            }}
-        />
-    );
+				await login({ ...data, device_name });
+				history.push("/");
+			}}
+		/>
+	);
 }
diff --git a/src/pages/login/forms/FormResend.tsx b/src/pages/login/forms/FormResend.tsx
index 643767d6f664801924bdb305677e2e1deb75593c..da1e27e5f521fc375a7bbb5e1e61bbe0ee3627ed 100644
--- a/src/pages/login/forms/FormResend.tsx
+++ b/src/pages/login/forms/FormResend.tsx
@@ -1,16 +1,18 @@
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
 import { useContext } from "preact/hooks";
+
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
 import { Form } from "./Form";
 
 export function FormResend() {
-    const client = useContext(AppContext);
+	const client = useContext(AppContext);
 
-    return (
-        <Form
-            page="resend"
-            callback={async data => {
-                await client.req("POST", "/auth/resend", data);
-            }}
-        />
-    );
+	return (
+		<Form
+			page="resend"
+			callback={async (data) => {
+				await client.req("POST", "/auth/resend", data);
+			}}
+		/>
+	);
 }
diff --git a/src/pages/login/forms/FormReset.tsx b/src/pages/login/forms/FormReset.tsx
index ac73d0186563e03c1e62b9dd4208877a9c8cca17..8e265688f443ba687d0e642575d8fddfc89a6af0 100644
--- a/src/pages/login/forms/FormReset.tsx
+++ b/src/pages/login/forms/FormReset.tsx
@@ -1,36 +1,39 @@
-import { Form } from "./Form";
-import { useContext } from "preact/hooks";
 import { useHistory, useParams } from "react-router-dom";
+
+import { useContext } from "preact/hooks";
+
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 
+import { Form } from "./Form";
+
 export function FormSendReset() {
-    const client = useContext(AppContext);
+	const client = useContext(AppContext);
 
-    return (
-        <Form
-            page="send_reset"
-            callback={async data => {
-                await client.req("POST", "/auth/send_reset", data);
-            }}
-        />
-    );
+	return (
+		<Form
+			page="send_reset"
+			callback={async (data) => {
+				await client.req("POST", "/auth/send_reset", data);
+			}}
+		/>
+	);
 }
 
 export function FormReset() {
-    const { token } = useParams<{ token: string }>();
-    const client = useContext(AppContext);
-    const history = useHistory();
+	const { token } = useParams<{ token: string }>();
+	const client = useContext(AppContext);
+	const history = useHistory();
 
-    return (
-        <Form
-            page="reset"
-            callback={async data => {
-                await client.req("POST", "/auth/reset", {
-                    token,
-                    ...data
-                });
-                history.push("/login");
-            }}
-        />
-    );
+	return (
+		<Form
+			page="reset"
+			callback={async (data) => {
+				await client.req("POST", "/auth/reset", {
+					token,
+					...data,
+				});
+				history.push("/login");
+			}}
+		/>
+	);
 }
diff --git a/src/pages/login/forms/Legal.tsx b/src/pages/login/forms/Legal.tsx
index 2517352c1d8bec309364e78af102d63f6cb362f5..754429b8f7f41777ff65ccb932acb823bdefc7e5 100644
--- a/src/pages/login/forms/Legal.tsx
+++ b/src/pages/login/forms/Legal.tsx
@@ -2,28 +2,19 @@ import styles from "../Login.module.scss";
 import { Text } from "preact-i18n";
 
 export function Legal() {
-    return (
-        <span className={styles.footer}>
-            <a
-                href="https://revolt.chat/about"
-                target="_blank"
-            >
-                <Text id="general.about" />
-            </a>
-            &middot;
-            <a
-                href="https://revolt.chat/terms"
-                target="_blank"
-            >
-                <Text id="general.tos" />
-            </a>
-            &middot;
-            <a
-                href="https://revolt.chat/privacy"
-                target="_blank"
-            >
-                <Text id="general.privacy" />
-            </a>
-        </span>
-    );
+	return (
+		<span className={styles.footer}>
+			<a href="https://revolt.chat/about" target="_blank">
+				<Text id="general.about" />
+			</a>
+			&middot;
+			<a href="https://revolt.chat/terms" target="_blank">
+				<Text id="general.tos" />
+			</a>
+			&middot;
+			<a href="https://revolt.chat/privacy" target="_blank">
+				<Text id="general.privacy" />
+			</a>
+		</span>
+	);
 }
diff --git a/src/pages/login/forms/MailProvider.tsx b/src/pages/login/forms/MailProvider.tsx
index 261857ba1dc545fc7ddfeea0e681af4c6f76d6f0..8475f03c4a84cd4b5eab5c8a6db7d29255ee680c 100644
--- a/src/pages/login/forms/MailProvider.tsx
+++ b/src/pages/login/forms/MailProvider.tsx
@@ -1,55 +1,56 @@
-import { Text } from "preact-i18n";
 import styles from "../Login.module.scss";
+import { Text } from "preact-i18n";
+
 import Button from "../../../components/ui/Button";
 
 interface Props {
-    email?: string;
+	email?: string;
 }
 
 function mapMailProvider(email?: string): [string, string] | undefined {
-    if (!email) return;
+	if (!email) return;
 
-    const match = /@(.+)/.exec(email);
-    if (match === null) return;
+	const match = /@(.+)/.exec(email);
+	if (match === null) return;
 
-    const domain = match[1];
-    switch (domain) {
-        case "gmail.com":
-            return ["Gmail", "https://gmail.com"];
-        case "tuta.io":
-            return ["Tutanota", "https://mail.tutanota.com"];
-        case "outlook.com":
-            return ["Outlook", "https://outlook.live.com"];
-        case "yahoo.com":
-            return ["Yahoo", "https://mail.yahoo.com"];
-        case "wp.pl":
-            return ["WP Poczta", "https://poczta.wp.pl"];
-        case "protonmail.com":
-        case "protonmail.ch":
-            return ["ProtonMail", "https://mail.protonmail.com"];
-        case "seznam.cz":
-        case "email.cz":
-        case "post.cz":
-            return ["Seznam", "https://email.seznam.cz"];
-        default:
-            return [domain, `https://${domain}`];
-    }
+	const domain = match[1];
+	switch (domain) {
+		case "gmail.com":
+			return ["Gmail", "https://gmail.com"];
+		case "tuta.io":
+			return ["Tutanota", "https://mail.tutanota.com"];
+		case "outlook.com":
+			return ["Outlook", "https://outlook.live.com"];
+		case "yahoo.com":
+			return ["Yahoo", "https://mail.yahoo.com"];
+		case "wp.pl":
+			return ["WP Poczta", "https://poczta.wp.pl"];
+		case "protonmail.com":
+		case "protonmail.ch":
+			return ["ProtonMail", "https://mail.protonmail.com"];
+		case "seznam.cz":
+		case "email.cz":
+		case "post.cz":
+			return ["Seznam", "https://email.seznam.cz"];
+		default:
+			return [domain, `https://${domain}`];
+	}
 }
 
 export function MailProvider({ email }: Props) {
-    const provider = mapMailProvider(email);
-    if (!provider) return null;
+	const provider = mapMailProvider(email);
+	if (!provider) return null;
 
-    return (
-        <div className={styles.mailProvider}>
-            <a href={provider[1]} target="_blank">
-                <Button>
-                    <Text
-                        id="login.open_mail_provider"
-                        fields={{ provider: provider[0] }}
-                    />
-                </Button>
-            </a>
-        </div>
-    );
+	return (
+		<div className={styles.mailProvider}>
+			<a href={provider[1]} target="_blank">
+				<Button>
+					<Text
+						id="login.open_mail_provider"
+						fields={{ provider: provider[0] }}
+					/>
+				</Button>
+			</a>
+		</div>
+	);
 }
diff --git a/src/pages/settings/ChannelSettings.tsx b/src/pages/settings/ChannelSettings.tsx
index aed5ca6950b2c6164bb5c516382e3609611064e7..6720af598aee77cb8552bf5bffa758f1b43c8c5c 100644
--- a/src/pages/settings/ChannelSettings.tsx
+++ b/src/pages/settings/ChannelSettings.tsx
@@ -1,62 +1,87 @@
-import { Text } from "preact-i18n";
-import Category from "../../components/ui/Category";
-import { GenericSettings } from "./GenericSettings";
-import { getChannelName } from "../../context/revoltjs/util";
-import { Route, useHistory, useParams } from "react-router-dom";
 import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
+import { Route, useHistory, useParams } from "react-router-dom";
+
+import { Text } from "preact-i18n";
+
 import { useChannel, useForceUpdate } from "../../context/revoltjs/hooks";
+import { getChannelName } from "../../context/revoltjs/util";
+
+import Category from "../../components/ui/Category";
 
+import { GenericSettings } from "./GenericSettings";
 import Overview from "./channel/Overview";
 import Permissions from "./channel/Permissions";
 
 export default function ChannelSettings() {
-    const { channel: cid } = useParams<{ channel: string; }>();
-    const ctx = useForceUpdate();
-    const channel = useChannel(cid, ctx);
-    if (!channel) return null;
-    if (channel.channel_type === 'SavedMessages' || channel.channel_type === 'DirectMessage') return null;
-
-    const history = useHistory();
-    function switchPage(to?: string) {
-        let base_url;
-        switch (channel?.channel_type) {
-            case 'TextChannel':
-            case 'VoiceChannel': base_url = `/server/${channel.server}/channel/${cid}/settings`; break;
-            default: base_url = `/channel/${cid}/settings`;
-        }
-
-        if (to) {
-            history.replace(`${base_url}/${to}`);
-        } else {
-            history.replace(base_url);
-        }
-    }
-
-    return (
-        <GenericSettings
-            pages={[
-                {
-                    category: <Category variant="uniform" text={getChannelName(ctx.client, channel, true)} />,
-                    id: 'overview',
-                    icon: <ListUl size={20} />,
-                    title: <Text id="app.settings.channel_pages.overview.title" />
-                },
-                {
-                    id: 'permissions',
-                    icon: <ListCheck size={20} />,
-                    title: <Text id="app.settings.channel_pages.permissions.title" />
-                }
-            ]}
-            children={[
-                <Route path="/server/:server/channel/:channel/settings/permissions"><Permissions channel={channel} /></Route>,
-                <Route path="/channel/:channel/settings/permissions"><Permissions channel={channel} /></Route>,
-
-                <Route path="/"><Overview channel={channel} /></Route>
-            ]}
-            category="channel_pages"
-            switchPage={switchPage}
-            defaultPage="overview"
-            showExitButton
-        />
-    )
+	const { channel: cid } = useParams<{ channel: string }>();
+	const ctx = useForceUpdate();
+	const channel = useChannel(cid, ctx);
+	if (!channel) return null;
+	if (
+		channel.channel_type === "SavedMessages" ||
+		channel.channel_type === "DirectMessage"
+	)
+		return null;
+
+	const history = useHistory();
+	function switchPage(to?: string) {
+		let base_url;
+		switch (channel?.channel_type) {
+			case "TextChannel":
+			case "VoiceChannel":
+				base_url = `/server/${channel.server}/channel/${cid}/settings`;
+				break;
+			default:
+				base_url = `/channel/${cid}/settings`;
+		}
+
+		if (to) {
+			history.replace(`${base_url}/${to}`);
+		} else {
+			history.replace(base_url);
+		}
+	}
+
+	return (
+		<GenericSettings
+			pages={[
+				{
+					category: (
+						<Category
+							variant="uniform"
+							text={getChannelName(ctx.client, channel, true)}
+						/>
+					),
+					id: "overview",
+					icon: <ListUl size={20} />,
+					title: (
+						<Text id="app.settings.channel_pages.overview.title" />
+					),
+				},
+				{
+					id: "permissions",
+					icon: <ListCheck size={20} />,
+					title: (
+						<Text id="app.settings.channel_pages.permissions.title" />
+					),
+				},
+			]}
+			children={[
+				<Route path="/server/:server/channel/:channel/settings/permissions">
+					<Permissions channel={channel} />
+				</Route>,
+				<Route path="/channel/:channel/settings/permissions">
+					<Permissions channel={channel} />
+				</Route>,
+
+				<Route path="/">
+					<Overview channel={channel} />
+				</Route>,
+			]}
+			category="channel_pages"
+			switchPage={switchPage}
+			defaultPage="overview"
+			showExitButton
+		/>
+	);
 }
diff --git a/src/pages/settings/GenericSettings.tsx b/src/pages/settings/GenericSettings.tsx
index 484ea683cf6861b03aeb11e53bd88e477e599995..2f64ecc4d86790a165a160aac4885316ddc7a937 100644
--- a/src/pages/settings/GenericSettings.tsx
+++ b/src/pages/settings/GenericSettings.tsx
@@ -1,134 +1,158 @@
-import { Text } from "preact-i18n";
+import { ArrowBack, X, XCircle } from "@styled-icons/boxicons-regular";
 import { Helmet } from "react-helmet";
+import { Switch, useHistory, useParams } from "react-router-dom";
+
 import styles from "./Settings.module.scss";
-import { Children } from "../../types/Preact";
-import Header from '../../components/ui/Header';
-import { ThemeContext } from "../../context/Theme";
-import Category from '../../components/ui/Category';
+import { Text } from "preact-i18n";
 import { useContext, useEffect } from "preact/hooks";
+
+import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+
+import { ThemeContext } from "../../context/Theme";
+
+import Category from "../../components/ui/Category";
+import Header from "../../components/ui/Header";
 import IconButton from "../../components/ui/IconButton";
 import LineDivider from "../../components/ui/LineDivider";
-import { Switch, useHistory, useParams } from "react-router-dom";
-import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
+
 import ButtonItem from "../../components/navigation/items/ButtonItem";
-import { ArrowBack, X, XCircle } from "@styled-icons/boxicons-regular";
+import { Children } from "../../types/Preact";
 
 interface Props {
-    pages: {
-        category?: Children
-        divider?: boolean
-        id: string
-        icon: Children
-        title: Children
-        hideTitle?: boolean
-    }[]
-    custom?: Children
-    children: Children
-    defaultPage: string
-    showExitButton?: boolean
-    switchPage: (to?: string) => void
-    category: 'pages' | 'channel_pages' | 'server_pages'
+	pages: {
+		category?: Children;
+		divider?: boolean;
+		id: string;
+		icon: Children;
+		title: Children;
+		hideTitle?: boolean;
+	}[];
+	custom?: Children;
+	children: Children;
+	defaultPage: string;
+	showExitButton?: boolean;
+	switchPage: (to?: string) => void;
+	category: "pages" | "channel_pages" | "server_pages";
 }
 
-export function GenericSettings({ pages, switchPage, category, custom, children, defaultPage, showExitButton }: Props) {
-    const history = useHistory();
-    const theme = useContext(ThemeContext);
-    const { page } = useParams<{ page: string; }>();
+export function GenericSettings({
+	pages,
+	switchPage,
+	category,
+	custom,
+	children,
+	defaultPage,
+	showExitButton,
+}: Props) {
+	const history = useHistory();
+	const theme = useContext(ThemeContext);
+	const { page } = useParams<{ page: string }>();
 
-    function exitSettings() {
-        if (history.length > 0) {
-            history.goBack();
-        } else {
-            history.push('/');
-        }
-    }
+	function exitSettings() {
+		if (history.length > 0) {
+			history.goBack();
+		} else {
+			history.push("/");
+		}
+	}
 
-    useEffect(() => {
-        function keyDown(e: KeyboardEvent) {
-            if (e.key === "Escape") {
-                exitSettings();
-            }
-        }
+	useEffect(() => {
+		function keyDown(e: KeyboardEvent) {
+			if (e.key === "Escape") {
+				exitSettings();
+			}
+		}
 
-        document.body.addEventListener("keydown", keyDown);
-        return () => document.body.removeEventListener("keydown", keyDown);
-    }, []);
+		document.body.addEventListener("keydown", keyDown);
+		return () => document.body.removeEventListener("keydown", keyDown);
+	}, []);
 
-    return (
-        <div className={styles.settings} data-mobile={isTouchscreenDevice}>
-            <Helmet>
-                <meta
-                    name="theme-color"
-                    content={
-                        isTouchscreenDevice
-                            ? theme["primary-header"]
-                            : theme["secondary-background"]
-                    }
-                />
-            </Helmet>
-            {isTouchscreenDevice && (
-                <Header placement="primary">
-                    {typeof page === "undefined" ? (
-                        <>
-                            { showExitButton &&
-                                <IconButton onClick={exitSettings}>
-                                    <X size={24} />
-                                </IconButton> }
-                            <Text id="app.settings.title" />
-                        </>
-                    ) : (
-                        <>
-                            <IconButton onClick={() => switchPage()}>
-                                <ArrowBack size={24} />
-                            </IconButton>
-                            <Text
-                                id={`app.settings.${category}.${page}.title`}
-                            />
-                        </>
-                    )}
-                </Header>
-            )}
-            {(!isTouchscreenDevice || typeof page === "undefined") && (
-                <div className={styles.sidebar}>
-                    <div className={styles.container}>
-                        {
-                            pages.map((entry, i) =>
-                                <>
-                                    { entry.category && <Category variant="uniform" text={entry.category} /> }
-                                    <ButtonItem
-                                        active={page === entry.id || (i === 0 && !isTouchscreenDevice && typeof page === "undefined")}
-                                        onClick={() => switchPage(entry.id)}
-                                        compact
-                                    >{entry.icon} {entry.title}</ButtonItem>
-                                    { entry.divider && <LineDivider /> }
-                                </>
-                            )
-                        }
-                        { custom }
-                    </div>
-                </div>
-            )}
-            {(!isTouchscreenDevice || typeof page === "string") && (
-                <div className={styles.content}>
-                    {!isTouchscreenDevice && !(pages.find(x => x.id === page && x.hideTitle)) && (
-                        <h1>
-                            <Text
-                                id={`app.settings.${category}.${page ?? defaultPage}.title`}
-                            />
-                        </h1>
-                    )}
-                    <Switch>
-                        { children }
-                    </Switch>
-                </div>
-            )}
-            {!isTouchscreenDevice && (
-                <div className={styles.action}>
-                    <IconButton onClick={exitSettings}>
-                        <XCircle size={48} />
-                    </IconButton>
-                </div>
-            )}
-        </div>
-    );
+	return (
+		<div className={styles.settings} data-mobile={isTouchscreenDevice}>
+			<Helmet>
+				<meta
+					name="theme-color"
+					content={
+						isTouchscreenDevice
+							? theme["primary-header"]
+							: theme["secondary-background"]
+					}
+				/>
+			</Helmet>
+			{isTouchscreenDevice && (
+				<Header placement="primary">
+					{typeof page === "undefined" ? (
+						<>
+							{showExitButton && (
+								<IconButton onClick={exitSettings}>
+									<X size={24} />
+								</IconButton>
+							)}
+							<Text id="app.settings.title" />
+						</>
+					) : (
+						<>
+							<IconButton onClick={() => switchPage()}>
+								<ArrowBack size={24} />
+							</IconButton>
+							<Text
+								id={`app.settings.${category}.${page}.title`}
+							/>
+						</>
+					)}
+				</Header>
+			)}
+			{(!isTouchscreenDevice || typeof page === "undefined") && (
+				<div className={styles.sidebar}>
+					<div className={styles.container}>
+						{pages.map((entry, i) => (
+							<>
+								{entry.category && (
+									<Category
+										variant="uniform"
+										text={entry.category}
+									/>
+								)}
+								<ButtonItem
+									active={
+										page === entry.id ||
+										(i === 0 &&
+											!isTouchscreenDevice &&
+											typeof page === "undefined")
+									}
+									onClick={() => switchPage(entry.id)}
+									compact>
+									{entry.icon} {entry.title}
+								</ButtonItem>
+								{entry.divider && <LineDivider />}
+							</>
+						))}
+						{custom}
+					</div>
+				</div>
+			)}
+			{(!isTouchscreenDevice || typeof page === "string") && (
+				<div className={styles.content}>
+					{!isTouchscreenDevice &&
+						!pages.find((x) => x.id === page && x.hideTitle) && (
+							<h1>
+								<Text
+									id={`app.settings.${category}.${
+										page ?? defaultPage
+									}.title`}
+								/>
+							</h1>
+						)}
+					<Switch>{children}</Switch>
+				</div>
+			)}
+			{!isTouchscreenDevice && (
+				<div className={styles.action}>
+					<IconButton onClick={exitSettings}>
+						<XCircle size={48} />
+					</IconButton>
+				</div>
+			)}
+		</div>
+	);
 }
diff --git a/src/pages/settings/ServerSettings.tsx b/src/pages/settings/ServerSettings.tsx
index 2c5148a7246250526735325fe25103696a72f048..bb0cde830583f8f41d25d7ae3081bce59451a6ad 100644
--- a/src/pages/settings/ServerSettings.tsx
+++ b/src/pages/settings/ServerSettings.tsx
@@ -1,74 +1,106 @@
-import { Text } from "preact-i18n";
-import Category from "../../components/ui/Category";
-import { GenericSettings } from "./GenericSettings";
-import { useServer } from "../../context/revoltjs/hooks";
-import { Route, useHistory, useParams } from "react-router-dom";
-import { ListUl, Share, Group, ListCheck } from "@styled-icons/boxicons-regular";
+import {
+	ListUl,
+	Share,
+	Group,
+	ListCheck,
+} from "@styled-icons/boxicons-regular";
 import { XSquare } from "@styled-icons/boxicons-solid";
+import { Route, useHistory, useParams } from "react-router-dom";
+
+import { Text } from "preact-i18n";
+
 import RequiresOnline from "../../context/revoltjs/RequiresOnline";
+import { useServer } from "../../context/revoltjs/hooks";
 
-import { Overview } from "./server/Overview";
-import { Members } from "./server/Members";
-import { Invites } from "./server/Invites";
+import Category from "../../components/ui/Category";
+
+import { GenericSettings } from "./GenericSettings";
 import { Bans } from "./server/Bans";
+import { Invites } from "./server/Invites";
+import { Members } from "./server/Members";
+import { Overview } from "./server/Overview";
 import { Roles } from "./server/Roles";
 
 export default function ServerSettings() {
-    const { server: sid } = useParams<{ server: string; }>();
-    const server = useServer(sid);
-    if (!server) return null;
+	const { server: sid } = useParams<{ server: string }>();
+	const server = useServer(sid);
+	if (!server) return null;
 
-    const history = useHistory();
-    function switchPage(to?: string) {
-        if (to) {
-            history.replace(`/server/${sid}/settings/${to}`);
-        } else {
-            history.replace(`/server/${sid}/settings`);
-        }
-    }
+	const history = useHistory();
+	function switchPage(to?: string) {
+		if (to) {
+			history.replace(`/server/${sid}/settings/${to}`);
+		} else {
+			history.replace(`/server/${sid}/settings`);
+		}
+	}
 
-    return (
-        <GenericSettings
-            pages={[
-                {
-                    category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
-                    id: 'overview',
-                    icon: <ListUl size={20} />,
-                    title: <Text id="app.settings.server_pages.overview.title" />
-                },
-                {
-                    id: 'members',
-                    icon: <Group size={20} />,
-                    title: <Text id="app.settings.server_pages.members.title" />
-                },
-                {
-                    id: 'invites',
-                    icon: <Share size={20} />,
-                    title: <Text id="app.settings.server_pages.invites.title" />
-                },
-                {
-                    id: 'bans',
-                    icon: <XSquare size={20} />,
-                    title: <Text id="app.settings.server_pages.bans.title" />
-                },
-                {
-                    id: 'roles',
-                    icon: <ListCheck size={20} />,
-                    title: <Text id="app.settings.server_pages.roles.title" />,
-                    hideTitle: true
-                }
-            ]}
-            children={[
-                <Route path="/server/:server/settings/members"><RequiresOnline><Members server={server} /></RequiresOnline></Route>,
-                <Route path="/server/:server/settings/invites"><RequiresOnline><Invites server={server} /></RequiresOnline></Route>,
-                <Route path="/server/:server/settings/bans"><RequiresOnline><Bans server={server} /></RequiresOnline></Route>,
-                <Route path="/server/:server/settings/roles"><RequiresOnline><Roles server={server} /></RequiresOnline></Route>,
-                <Route path="/"><Overview server={server} /></Route>
-            ]}
-            category="server_pages"
-            switchPage={switchPage}
-            defaultPage="overview"
-            showExitButton
-        />
-    )
+	return (
+		<GenericSettings
+			pages={[
+				{
+					category: <Category variant="uniform" text={server.name} />, //TOFIX: Just add the server.name as a string, otherwise it makes a duplicate category
+					id: "overview",
+					icon: <ListUl size={20} />,
+					title: (
+						<Text id="app.settings.server_pages.overview.title" />
+					),
+				},
+				{
+					id: "members",
+					icon: <Group size={20} />,
+					title: (
+						<Text id="app.settings.server_pages.members.title" />
+					),
+				},
+				{
+					id: "invites",
+					icon: <Share size={20} />,
+					title: (
+						<Text id="app.settings.server_pages.invites.title" />
+					),
+				},
+				{
+					id: "bans",
+					icon: <XSquare size={20} />,
+					title: <Text id="app.settings.server_pages.bans.title" />,
+				},
+				{
+					id: "roles",
+					icon: <ListCheck size={20} />,
+					title: <Text id="app.settings.server_pages.roles.title" />,
+					hideTitle: true,
+				},
+			]}
+			children={[
+				<Route path="/server/:server/settings/members">
+					<RequiresOnline>
+						<Members server={server} />
+					</RequiresOnline>
+				</Route>,
+				<Route path="/server/:server/settings/invites">
+					<RequiresOnline>
+						<Invites server={server} />
+					</RequiresOnline>
+				</Route>,
+				<Route path="/server/:server/settings/bans">
+					<RequiresOnline>
+						<Bans server={server} />
+					</RequiresOnline>
+				</Route>,
+				<Route path="/server/:server/settings/roles">
+					<RequiresOnline>
+						<Roles server={server} />
+					</RequiresOnline>
+				</Route>,
+				<Route path="/">
+					<Overview server={server} />
+				</Route>,
+			]}
+			category="server_pages"
+			switchPage={switchPage}
+			defaultPage="overview"
+			showExitButton
+		/>
+	);
 }
diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx
index 4fa2b5827333b1fb9f186fe5176263182f3a0e92..fba14e3b1567973bd10508543cb7d582cc14a9d2 100644
--- a/src/pages/settings/Settings.tsx
+++ b/src/pages/settings/Settings.tsx
@@ -1,160 +1,202 @@
+import { Gitlab } from "@styled-icons/boxicons-logos";
+import {
+	Sync as SyncIcon,
+	Globe,
+	LogOut,
+} from "@styled-icons/boxicons-regular";
+import {
+	Bell,
+	Palette,
+	Coffee,
+	IdCard,
+	CheckShield,
+	Flask,
+	User,
+	Megaphone,
+} from "@styled-icons/boxicons-solid";
+import { Route, useHistory } from "react-router-dom";
+import { LIBRARY_VERSION } from "revolt.js";
+
+import styles from "./Settings.module.scss";
 import { Text } from "preact-i18n";
-import { Sync } from "./panes/Sync";
 import { useContext } from "preact/hooks";
-import styles from "./Settings.module.scss";
-import { LIBRARY_VERSION } from "revolt.js";
-import { APP_VERSION } from "../../version";
-import { GenericSettings } from "./GenericSettings";
-import { Route, useHistory } from "react-router-dom";
+
+import RequiresOnline from "../../context/revoltjs/RequiresOnline";
 import {
-    Bell,
-    Palette,
-    Coffee,
-    IdCard,
-    CheckShield,
-    Flask,
-    User,
-    Megaphone
-} from "@styled-icons/boxicons-solid";
-import { Sync as SyncIcon, Globe, LogOut } from "@styled-icons/boxicons-regular";
-import { Gitlab } from "@styled-icons/boxicons-logos";
-import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision";
+	AppContext,
+	OperationsContext,
+} from "../../context/revoltjs/RevoltClient";
+
 import LineDivider from "../../components/ui/LineDivider";
-import RequiresOnline from "../../context/revoltjs/RequiresOnline";
+
 import ButtonItem from "../../components/navigation/items/ButtonItem";
-import { AppContext, OperationsContext } from "../../context/revoltjs/RevoltClient";
+import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision";
+import { APP_VERSION } from "../../version";
+import { GenericSettings } from "./GenericSettings";
 import { Account } from "./panes/Account";
-import { Profile } from "./panes/Profile";
-import { Sessions } from "./panes/Sessions";
+import { Appearance } from "./panes/Appearance";
+import { ExperimentsPage } from "./panes/Experiments";
 import { Feedback } from "./panes/Feedback";
 import { Languages } from "./panes/Languages";
-import { Appearance } from "./panes/Appearance";
 import { Notifications } from "./panes/Notifications";
-import { ExperimentsPage } from "./panes/Experiments";
+import { Profile } from "./panes/Profile";
+import { Sessions } from "./panes/Sessions";
+import { Sync } from "./panes/Sync";
 
 export default function Settings() {
-    const history = useHistory();
-    const client = useContext(AppContext);
-    const operations = useContext(OperationsContext);
-    
-    function switchPage(to?: string) {
-        if (to) {
-            history.replace(`/settings/${to}`);
-        } else {
-            history.replace(`/settings`);
-        }
-    }
+	const history = useHistory();
+	const client = useContext(AppContext);
+	const operations = useContext(OperationsContext);
+
+	function switchPage(to?: string) {
+		if (to) {
+			history.replace(`/settings/${to}`);
+		} else {
+			history.replace(`/settings`);
+		}
+	}
 
-    return (
-        <GenericSettings
-            pages={[
-                {
-                    category: <Text id="app.settings.categories.user_settings" />,
-                    id: 'account',
-                    icon: <User size={20} />,
-                    title: <Text id="app.settings.pages.account.title" />
-                },
-                {
-                    id: 'profile',
-                    icon: <IdCard size={20} />,
-                    title: <Text id="app.settings.pages.profile.title" />
-                },
-                {
-                    id: 'sessions',
-                    icon: <CheckShield size={20} />,
-                    title: <Text id="app.settings.pages.sessions.title" />
-                },
-                {
-                    category: <Text id="app.settings.categories.client_settings" />,
-                    id: 'appearance',
-                    icon: <Palette size={20} />,
-                    title: <Text id="app.settings.pages.appearance.title" />
-                },
-                {
-                    id: 'notifications',
-                    icon: <Bell size={20} />,
-                    title: <Text id="app.settings.pages.notifications.title" />
-                },
-                {
-                    id: 'language',
-                    icon: <Globe size={20} />,
-                    title: <Text id="app.settings.pages.language.title" />
-                },
-                {
-                    id: 'sync',
-                    icon: <SyncIcon size={20} />,
-                    title: <Text id="app.settings.pages.sync.title" />
-                },
-                {
-                    divider: true,
-                    id: 'experiments',
-                    icon: <Flask size={20} />,
-                    title: <Text id="app.settings.pages.experiments.title" />
-                },
-                {
-                    id: 'feedback',
-                    icon: <Megaphone size={20} />,
-                    title: <Text id="app.settings.pages.feedback.title" />
-                }
-            ]}
-            children={[
-                <Route path="/settings/profile"><Profile /></Route>,
-                <Route path="/settings/sessions">
-                    <RequiresOnline><Sessions /></RequiresOnline>
-                </Route>,
-                <Route path="/settings/appearance"><Appearance /></Route>,
-                <Route path="/settings/notifications"><Notifications /></Route>,
-                <Route path="/settings/language"><Languages /></Route>,
-                <Route path="/settings/sync"><Sync /></Route>,
-                <Route path="/settings/experiments"><ExperimentsPage /></Route>,
-                <Route path="/settings/feedback"><Feedback /></Route>,
-                <Route path="/"><Account /></Route>
-            ]}
-            defaultPage="account"
-            switchPage={switchPage}
-            category="pages"
-            custom={[
-                <a
-                    href="https://gitlab.insrt.uk/revolt"
-                    target="_blank"
-                >
-                    <ButtonItem compact>
-                        <Gitlab size={20} />
-                        <Text id="app.settings.pages.source_code" />
-                    </ButtonItem>
-                </a>,
-                <a href="https://ko-fi.com/insertish" target="_blank">
-                    <ButtonItem className={styles.donate} compact>
-                        <Coffee size={20} />
-                        <Text id="app.settings.pages.donate.title" />
-                    </ButtonItem>
-                </a>,
-                <LineDivider />,
-                <ButtonItem
-                    onClick={() => operations.logout()}
-                    className={styles.logOut}
-                    compact
-                >
-                    <LogOut size={20} />
-                    <Text id="app.settings.pages.logOut" />
-                </ButtonItem>,
-                <div className={styles.version}>
-                    <div>
-                        <span className={styles.revision}>
-                            <a href={`${REPO_URL}/${GIT_REVISION}`} target="_blank">
-                                { GIT_REVISION.substr(0, 7) }
-                            </a>
-                            {` `}
-                            <a href={GIT_BRANCH !== 'DETACHED' ? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}` : undefined} target="_blank">
-                                ({ GIT_BRANCH })
-                            </a>
-                        </span>
-                        <span>{ GIT_BRANCH === 'production' ? 'Stable' : 'Nightly' } {APP_VERSION}</span>
-                        <span>API: {client.configuration?.revolt ?? "N/A"}</span>
-                        <span>revolt.js: {LIBRARY_VERSION}</span>
-                    </div>
-                </div>
-            ]}
-        />
-    )
+	return (
+		<GenericSettings
+			pages={[
+				{
+					category: (
+						<Text id="app.settings.categories.user_settings" />
+					),
+					id: "account",
+					icon: <User size={20} />,
+					title: <Text id="app.settings.pages.account.title" />,
+				},
+				{
+					id: "profile",
+					icon: <IdCard size={20} />,
+					title: <Text id="app.settings.pages.profile.title" />,
+				},
+				{
+					id: "sessions",
+					icon: <CheckShield size={20} />,
+					title: <Text id="app.settings.pages.sessions.title" />,
+				},
+				{
+					category: (
+						<Text id="app.settings.categories.client_settings" />
+					),
+					id: "appearance",
+					icon: <Palette size={20} />,
+					title: <Text id="app.settings.pages.appearance.title" />,
+				},
+				{
+					id: "notifications",
+					icon: <Bell size={20} />,
+					title: <Text id="app.settings.pages.notifications.title" />,
+				},
+				{
+					id: "language",
+					icon: <Globe size={20} />,
+					title: <Text id="app.settings.pages.language.title" />,
+				},
+				{
+					id: "sync",
+					icon: <SyncIcon size={20} />,
+					title: <Text id="app.settings.pages.sync.title" />,
+				},
+				{
+					divider: true,
+					id: "experiments",
+					icon: <Flask size={20} />,
+					title: <Text id="app.settings.pages.experiments.title" />,
+				},
+				{
+					id: "feedback",
+					icon: <Megaphone size={20} />,
+					title: <Text id="app.settings.pages.feedback.title" />,
+				},
+			]}
+			children={[
+				<Route path="/settings/profile">
+					<Profile />
+				</Route>,
+				<Route path="/settings/sessions">
+					<RequiresOnline>
+						<Sessions />
+					</RequiresOnline>
+				</Route>,
+				<Route path="/settings/appearance">
+					<Appearance />
+				</Route>,
+				<Route path="/settings/notifications">
+					<Notifications />
+				</Route>,
+				<Route path="/settings/language">
+					<Languages />
+				</Route>,
+				<Route path="/settings/sync">
+					<Sync />
+				</Route>,
+				<Route path="/settings/experiments">
+					<ExperimentsPage />
+				</Route>,
+				<Route path="/settings/feedback">
+					<Feedback />
+				</Route>,
+				<Route path="/">
+					<Account />
+				</Route>,
+			]}
+			defaultPage="account"
+			switchPage={switchPage}
+			category="pages"
+			custom={[
+				<a href="https://gitlab.insrt.uk/revolt" target="_blank">
+					<ButtonItem compact>
+						<Gitlab size={20} />
+						<Text id="app.settings.pages.source_code" />
+					</ButtonItem>
+				</a>,
+				<a href="https://ko-fi.com/insertish" target="_blank">
+					<ButtonItem className={styles.donate} compact>
+						<Coffee size={20} />
+						<Text id="app.settings.pages.donate.title" />
+					</ButtonItem>
+				</a>,
+				<LineDivider />,
+				<ButtonItem
+					onClick={() => operations.logout()}
+					className={styles.logOut}
+					compact>
+					<LogOut size={20} />
+					<Text id="app.settings.pages.logOut" />
+				</ButtonItem>,
+				<div className={styles.version}>
+					<div>
+						<span className={styles.revision}>
+							<a
+								href={`${REPO_URL}/${GIT_REVISION}`}
+								target="_blank">
+								{GIT_REVISION.substr(0, 7)}
+							</a>
+							{` `}
+							<a
+								href={
+									GIT_BRANCH !== "DETACHED"
+										? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
+										: undefined
+								}
+								target="_blank">
+								({GIT_BRANCH})
+							</a>
+						</span>
+						<span>
+							{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
+							{APP_VERSION}
+						</span>
+						<span>
+							API: {client.configuration?.revolt ?? "N/A"}
+						</span>
+						<span>revolt.js: {LIBRARY_VERSION}</span>
+					</div>
+				</div>,
+			]}
+		/>
+	);
 }
diff --git a/src/pages/settings/channel/Overview.tsx b/src/pages/settings/channel/Overview.tsx
index f41f123bd45242c04cf6556e68a4b03900d62a70..6df4e5a8a389bbca340476e6cec376d6c1a871e2 100644
--- a/src/pages/settings/channel/Overview.tsx
+++ b/src/pages/settings/channel/Overview.tsx
@@ -1,91 +1,117 @@
-import { Text } from "preact-i18n";
-import styles from "./Panes.module.scss";
-import Button from "../../../components/ui/Button";
 import { Channels } from "revolt.js/dist/api/objects";
-import InputBox from "../../../components/ui/InputBox";
-import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
+
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
+
 import { FileUploader } from "../../../context/revoltjs/FileUploads";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import Button from "../../../components/ui/Button";
+import InputBox from "../../../components/ui/InputBox";
 
 interface Props {
-    channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel;
+	channel:
+		| Channels.GroupChannel
+		| Channels.TextChannel
+		| Channels.VoiceChannel;
 }
 
 export default function Overview({ channel }: Props) {
-    const client = useContext(AppContext);
+	const client = useContext(AppContext);
 
-    const [name, setName] = useState(channel.name);
-    const [description, setDescription] = useState(channel.description ?? '');
+	const [name, setName] = useState(channel.name);
+	const [description, setDescription] = useState(channel.description ?? "");
 
-    useEffect(() => setName(channel.name), [ channel.name ]);
-    useEffect(() => setDescription(channel.description ?? ''), [ channel.description ]);
+	useEffect(() => setName(channel.name), [channel.name]);
+	useEffect(
+		() => setDescription(channel.description ?? ""),
+		[channel.description],
+	);
 
-    const [ changed, setChanged ] = useState(false);
-    function save() {
-        let changes: any = {};
-        if (name !== channel.name) changes.name = name;
-        if (description !== channel.description)
-            changes.description = description;
+	const [changed, setChanged] = useState(false);
+	function save() {
+		let changes: any = {};
+		if (name !== channel.name) changes.name = name;
+		if (description !== channel.description)
+			changes.description = description;
 
-        client.channels.edit(channel._id, changes);
-        setChanged(false);
-    }
+		client.channels.edit(channel._id, changes);
+		setChanged(false);
+	}
 
-    return (
-        <div className={styles.overview}>
-            <div className={styles.row}>
-                <FileUploader
-                    width={80}
-                    height={80}
-                    style="icon"
-                    fileType="icons"
-                    behaviour="upload"
-                    maxFileSize={2_500_000}
-                    onUpload={icon => client.channels.edit(channel._id, { icon })}
-                    previewURL={client.channels.getIconURL(channel._id, { max_side: 256 }, true)}
-                    remove={() => client.channels.edit(channel._id, { remove: 'Icon' })}
-                    defaultPreview={channel.channel_type === 'Group' ? "/assets/group.png" : undefined}
-                />
-                <div className={styles.name}>
-                    <h3>
-                        { channel.channel_type === 'Group' ?
-                            <Text id="app.main.groups.name" /> :
-                            <Text id="app.main.servers.channel_name" /> }
-                    </h3>
-                    <InputBox
-                        contrast
-                        value={name}
-                        maxLength={32}
-                        onChange={e => {
-                            setName(e.currentTarget.value)
-                            if (!changed) setChanged(true)
-                        }}
-                    />
-                </div>
-            </div>
+	return (
+		<div className={styles.overview}>
+			<div className={styles.row}>
+				<FileUploader
+					width={80}
+					height={80}
+					style="icon"
+					fileType="icons"
+					behaviour="upload"
+					maxFileSize={2_500_000}
+					onUpload={(icon) =>
+						client.channels.edit(channel._id, { icon })
+					}
+					previewURL={client.channels.getIconURL(
+						channel._id,
+						{ max_side: 256 },
+						true,
+					)}
+					remove={() =>
+						client.channels.edit(channel._id, { remove: "Icon" })
+					}
+					defaultPreview={
+						channel.channel_type === "Group"
+							? "/assets/group.png"
+							: undefined
+					}
+				/>
+				<div className={styles.name}>
+					<h3>
+						{channel.channel_type === "Group" ? (
+							<Text id="app.main.groups.name" />
+						) : (
+							<Text id="app.main.servers.channel_name" />
+						)}
+					</h3>
+					<InputBox
+						contrast
+						value={name}
+						maxLength={32}
+						onChange={(e) => {
+							setName(e.currentTarget.value);
+							if (!changed) setChanged(true);
+						}}
+					/>
+				</div>
+			</div>
 
-            <h3>
-                { channel.channel_type === 'Group' ?
-                    <Text id="app.main.groups.description" /> :
-                    <Text id="app.main.servers.channel_description" /> }
-            </h3>
-            <TextAreaAutoSize
-                maxRows={10}
-                minHeight={60}
-                maxLength={1024}
-                value={description}
-                placeholder={"Add a description..."}
-                onChange={ev => {
-                    setDescription(ev.currentTarget.value);
-                    if (!changed) setChanged(true)
-                }}
-            />
-            <p>
-                <Button onClick={save} contrast disabled={!changed}>
-                    <Text id="app.special.modals.actions.save" />
-                </Button>
-            </p>
-        </div>
-    );
+			<h3>
+				{channel.channel_type === "Group" ? (
+					<Text id="app.main.groups.description" />
+				) : (
+					<Text id="app.main.servers.channel_description" />
+				)}
+			</h3>
+			<TextAreaAutoSize
+				maxRows={10}
+				minHeight={60}
+				maxLength={1024}
+				value={description}
+				placeholder={"Add a description..."}
+				onChange={(ev) => {
+					setDescription(ev.currentTarget.value);
+					if (!changed) setChanged(true);
+				}}
+			/>
+			<p>
+				<Button onClick={save} contrast disabled={!changed}>
+					<Text id="app.special.modals.actions.save" />
+				</Button>
+			</p>
+		</div>
+	);
 }
diff --git a/src/pages/settings/channel/Permissions.tsx b/src/pages/settings/channel/Permissions.tsx
index 7fc8c2e694790082a7c4cc103d3fc8e02dc9ba38..c5467215631b7616279cbdf186e9a75c7e7c3027 100644
--- a/src/pages/settings/channel/Permissions.tsx
+++ b/src/pages/settings/channel/Permissions.tsx
@@ -1,91 +1,111 @@
-import Tip from "../../../components/ui/Tip";
-import Button from "../../../components/ui/Button";
 import { Channels } from "revolt.js/dist/api/objects";
-import Checkbox from "../../../components/ui/Checkbox";
-import { useServer } from "../../../context/revoltjs/hooks";
-import { useContext, useEffect, useState } from "preact/hooks";
 import { ChannelPermission } from "revolt.js/dist/api/permissions";
+
+import { useContext, useEffect, useState } from "preact/hooks";
+
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import { useServer } from "../../../context/revoltjs/hooks";
+
+import Button from "../../../components/ui/Button";
+import Checkbox from "../../../components/ui/Checkbox";
+import Tip from "../../../components/ui/Tip";
 
 // ! FIXME: export from revolt.js
-const DEFAULT_PERMISSION_DM = ChannelPermission.View
-    + ChannelPermission.SendMessage
-    + ChannelPermission.ManageChannel
-    + ChannelPermission.VoiceCall
-    + ChannelPermission.InviteOthers
-    + ChannelPermission.EmbedLinks
-    + ChannelPermission.UploadFiles;
+const DEFAULT_PERMISSION_DM =
+	ChannelPermission.View +
+	ChannelPermission.SendMessage +
+	ChannelPermission.ManageChannel +
+	ChannelPermission.VoiceCall +
+	ChannelPermission.InviteOthers +
+	ChannelPermission.EmbedLinks +
+	ChannelPermission.UploadFiles;
 
 interface Props {
-    channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel;
+	channel:
+		| Channels.GroupChannel
+		| Channels.TextChannel
+		| Channels.VoiceChannel;
 }
 
 // ! FIXME: bad code :)
 export default function Permissions({ channel }: Props) {
-    const [ selected, setSelected ] = useState('default');
-    const client = useContext(AppContext);
+	const [selected, setSelected] = useState("default");
+	const client = useContext(AppContext);
+
+	type R = { name: string; permissions: number };
+	let roles: { [key: string]: R } = {};
+	if (channel.channel_type !== "Group") {
+		const server = useServer(channel.server);
+		const a = server?.roles ?? {};
+		for (let b of Object.keys(a)) {
+			roles[b] = {
+				name: a[b].name,
+				permissions: a[b].permissions[1],
+			};
+		}
+	}
 
-    type R = { name: string, permissions: number };
-    let roles: { [key: string]: R } = {};
-    if (channel.channel_type !== 'Group') {
-        const server = useServer(channel.server);
-        const a = server?.roles ?? {};
-        for (let b of Object.keys(a)) {
-            roles[b] = {
-                name: a[b].name,
-                permissions: a[b].permissions[1]
-            };
-        }
-    }
+	const keys = ["default", ...Object.keys(roles)];
 
-    const keys = [ 'default', ...Object.keys(roles) ];
+	const defaultRole = {
+		name: "Default",
+		permissions:
+			(channel.channel_type === "Group"
+				? channel.permissions
+				: channel.default_permissions) ?? DEFAULT_PERMISSION_DM,
+	};
+	const selectedRole = selected === "default" ? defaultRole : roles[selected];
 
-    const defaultRole = { name: 'Default', permissions: (channel.channel_type === 'Group' ? channel.permissions : channel.default_permissions) ?? DEFAULT_PERMISSION_DM };
-    const selectedRole = selected === 'default' ? defaultRole : roles[selected];
+	if (!selectedRole) {
+		useEffect(() => setSelected("default"), []);
+		return null;
+	}
 
-    if (!selectedRole) {
-        useEffect(() => setSelected('default'), [ ]);
-        return null;
-    }
+	const [p, setPerm] = useState(selectedRole.permissions >>> 0);
 
-    const [ p, setPerm ] = useState(selectedRole.permissions >>> 0);
+	useEffect(() => {
+		setPerm(selectedRole.permissions >>> 0);
+	}, [selected, selectedRole.permissions]);
 
-    useEffect(() => {
-        setPerm(selectedRole.permissions >>> 0);
-    }, [ selected, selectedRole.permissions ]);
-    
-    return (
-        <div>
-            <Tip warning>This section is under construction.</Tip>
-            <h2>select role</h2>
-            { selected }
-            { keys
-                .map(id => {
-                    let role: R = id === 'default' ? defaultRole : roles[id];
+	return (
+		<div>
+			<Tip warning>This section is under construction.</Tip>
+			<h2>select role</h2>
+			{selected}
+			{keys.map((id) => {
+				let role: R = id === "default" ? defaultRole : roles[id];
 
-                    return (
-                        <Checkbox checked={selected === id} onChange={selected => selected && setSelected(id)}>
-                            { role.name }
-                        </Checkbox>
-                    )
-                })
-            }
-            <h2>channel per??issions</h2>
-            { Object.keys(ChannelPermission)
-                .map(perm => {
-                    let value = ChannelPermission[perm as keyof typeof ChannelPermission];
-                    if (value & DEFAULT_PERMISSION_DM) {
-                        return (
-                            <Checkbox checked={(p & value) > 0} onChange={c => setPerm(c ? (p | value) : (p ^ value))}>
-                                { perm }
-                            </Checkbox>
-                        )
-                    }
-                })
-            }
-            <Button contrast onClick={() => {
-                client.channels.setPermissions(channel._id, selected, p);
-            }}>click here to save permissions for role</Button>
-        </div>
-    );
+				return (
+					<Checkbox
+						checked={selected === id}
+						onChange={(selected) => selected && setSelected(id)}>
+						{role.name}
+					</Checkbox>
+				);
+			})}
+			<h2>channel per??issions</h2>
+			{Object.keys(ChannelPermission).map((perm) => {
+				let value =
+					ChannelPermission[perm as keyof typeof ChannelPermission];
+				if (value & DEFAULT_PERMISSION_DM) {
+					return (
+						<Checkbox
+							checked={(p & value) > 0}
+							onChange={(c) =>
+								setPerm(c ? p | value : p ^ value)
+							}>
+							{perm}
+						</Checkbox>
+					);
+				}
+			})}
+			<Button
+				contrast
+				onClick={() => {
+					client.channels.setPermissions(channel._id, selected, p);
+				}}>
+				click here to save permissions for role
+			</Button>
+		</div>
+	);
 }
diff --git a/src/pages/settings/panes/Account.tsx b/src/pages/settings/panes/Account.tsx
index 88a7839c70b7e53a71451de484fb3a5624ed129d..08beb2e71b4bed0079d11607b4ae717e5b4ad548 100644
--- a/src/pages/settings/panes/Account.tsx
+++ b/src/pages/settings/panes/Account.tsx
@@ -1,94 +1,106 @@
-import { Text } from "preact-i18n";
-import styles from "./Panes.module.scss";
-import Tip from "../../../components/ui/Tip";
-import Button from "../../../components/ui/Button";
-import { Users } from "revolt.js/dist/api/objects";
-import { Link, useHistory } from "react-router-dom";
-import Overline from "../../../components/ui/Overline";
-import { Envelope, Key } from "@styled-icons/boxicons-solid";
 import { At } from "@styled-icons/boxicons-regular";
+import { Envelope, Key } from "@styled-icons/boxicons-solid";
+import { Link, useHistory } from "react-router-dom";
+import { Users } from "revolt.js/dist/api/objects";
+
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
-import UserIcon from "../../../components/common/user/UserIcon";
-import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
+
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
+import {
+	ClientStatus,
+	StatusContext,
+} from "../../../context/revoltjs/RevoltClient";
+import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
+
+import UserIcon from "../../../components/common/user/UserIcon";
+import Button from "../../../components/ui/Button";
+import Overline from "../../../components/ui/Overline";
+import Tip from "../../../components/ui/Tip";
 
 export function Account() {
-    const { openScreen } = useIntermediate();
-    const status = useContext(StatusContext);
+	const { openScreen } = useIntermediate();
+	const status = useContext(StatusContext);
 
-    const ctx = useForceUpdate();
-    const user = useSelf(ctx);
-    if (!user) return null;
+	const ctx = useForceUpdate();
+	const user = useSelf(ctx);
+	if (!user) return null;
 
-    const [email, setEmail] = useState("...");
-    const [profile, setProfile] = useState<undefined | Users.Profile>(
-        undefined
-    );
-    const history = useHistory();
+	const [email, setEmail] = useState("...");
+	const [profile, setProfile] = useState<undefined | Users.Profile>(
+		undefined,
+	);
+	const history = useHistory();
 
-    function switchPage(to: string) {
-        history.replace(`/settings/${to}`);
-    }
+	function switchPage(to: string) {
+		history.replace(`/settings/${to}`);
+	}
 
-    useEffect(() => {
-        if (email === "..." && status === ClientStatus.ONLINE) {
-            ctx.client
-                .req("GET", "/auth/user")
-                .then(account => setEmail(account.email));
-        }
+	useEffect(() => {
+		if (email === "..." && status === ClientStatus.ONLINE) {
+			ctx.client
+				.req("GET", "/auth/user")
+				.then((account) => setEmail(account.email));
+		}
 
-        if (profile === undefined && status === ClientStatus.ONLINE) {
-            ctx.client.users
-                .fetchProfile(user._id)
-                .then(profile => setProfile(profile ?? {}));
-        }
-    }, [status]);
+		if (profile === undefined && status === ClientStatus.ONLINE) {
+			ctx.client.users
+				.fetchProfile(user._id)
+				.then((profile) => setProfile(profile ?? {}));
+		}
+	}, [status]);
 
-    return (
-        <div className={styles.user}>
-            <div className={styles.banner}>
-                <UserIcon className={styles.avatar} target={user} size={72} onClick={() => switchPage("profile")}/>
-                <div className={styles.username}>@{user.username}</div>
-            </div>
-            <div className={styles.details}>
-                {([
-                    ["username", user.username, <At size={24} />],
-                    ["email", email, <Envelope size={24} />],
-                    ["password", "*****", <Key size={24} />]
-                ] as const).map(([field, value, icon]) => (
-                    <div>
-                        {icon}
-                        <div className={styles.detail}>
-                            <Overline>
-                                <Text id={`login.${field}`} />
-                            </Overline>
-                            <p>{value}</p>
-                        </div>
-                        <div>
-                            <Button
-                                onClick={() =>
-                                    openScreen({
-                                        id: "modify_account",
-                                        field: field
-                                    })
-                                }
-                                contrast
-                            >
-                                <Text id="app.settings.pages.account.change_field" />
-                            </Button>
-                        </div>
-                    </div>
-                ))}
-            </div>
-            <Tip>
-                <span>
-                    <Text id="app.settings.tips.account.a" />
-                </span>{" "}
-                <a onClick={() => switchPage("profile")}>
-                    <Text id="app.settings.tips.account.b" />
-                </a>
-            </Tip>
-        </div>
-    );
+	return (
+		<div className={styles.user}>
+			<div className={styles.banner}>
+				<UserIcon
+					className={styles.avatar}
+					target={user}
+					size={72}
+					onClick={() => switchPage("profile")}
+				/>
+				<div className={styles.username}>@{user.username}</div>
+			</div>
+			<div className={styles.details}>
+				{(
+					[
+						["username", user.username, <At size={24} />],
+						["email", email, <Envelope size={24} />],
+						["password", "*****", <Key size={24} />],
+					] as const
+				).map(([field, value, icon]) => (
+					<div>
+						{icon}
+						<div className={styles.detail}>
+							<Overline>
+								<Text id={`login.${field}`} />
+							</Overline>
+							<p>{value}</p>
+						</div>
+						<div>
+							<Button
+								onClick={() =>
+									openScreen({
+										id: "modify_account",
+										field: field,
+									})
+								}
+								contrast>
+								<Text id="app.settings.pages.account.change_field" />
+							</Button>
+						</div>
+					</div>
+				))}
+			</div>
+			<Tip>
+				<span>
+					<Text id="app.settings.tips.account.a" />
+				</span>{" "}
+				<a onClick={() => switchPage("profile")}>
+					<Text id="app.settings.tips.account.b" />
+				</a>
+			</Tip>
+		</div>
+	);
 }
diff --git a/src/pages/settings/panes/Appearance.tsx b/src/pages/settings/panes/Appearance.tsx
index 714e4086ec8a7bb04743dd6d2245236d5ee2886a..337e7451b426aa10244f12e069b0d9efff4a0925 100644
--- a/src/pages/settings/panes/Appearance.tsx
+++ b/src/pages/settings/panes/Appearance.tsx
@@ -1,116 +1,130 @@
-import { Text } from "preact-i18n";
+// @ts-ignore
+import pSBC from "shade-blend-color";
+
 import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
+import { useCallback, useContext, useEffect, useState } from "preact/hooks";
+
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
 import { debounce } from "../../../lib/debounce";
-import Button from "../../../components/ui/Button";
-import Checkbox from "../../../components/ui/Checkbox";
-import ComboBox from "../../../components/ui/ComboBox";
-import InputBox from "../../../components/ui/InputBox";
+
+import { dispatch } from "../../../redux";
 import { connectState } from "../../../redux/connector";
-import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
-import ColourSwatches from "../../../components/ui/ColourSwatches";
 import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
-import { useCallback, useContext, useEffect, useState } from "preact/hooks";
-import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import CollapsibleSection from "../../../components/common/CollapsibleSection";
-import { DEFAULT_FONT, DEFAULT_MONO_FONT, FONTS, FONT_KEYS, MONOSCAPE_FONTS, MONOSCAPE_FONT_KEYS, Theme, ThemeContext, ThemeOptions } from "../../../context/Theme";
-
-// @ts-ignore
-import pSBC from 'shade-blend-color';
 
-import lightSVG from '../assets/light.svg';
-import darkSVG from '../assets/dark.svg';
+import {
+	DEFAULT_FONT,
+	DEFAULT_MONO_FONT,
+	FONTS,
+	FONT_KEYS,
+	MONOSCAPE_FONTS,
+	MONOSCAPE_FONT_KEYS,
+	Theme,
+	ThemeContext,
+	ThemeOptions,
+} from "../../../context/Theme";
+import { useIntermediate } from "../../../context/intermediate/Intermediate";
 
-import mutantSVG from '../assets/mutant_emoji.svg';
-import notoSVG from '../assets/noto_emoji.svg';
-import openmojiSVG from '../assets/openmoji_emoji.svg';
-import twemojiSVG from '../assets/twemoji_emoji.svg';
-import { dispatch } from "../../../redux";
+import CollapsibleSection from "../../../components/common/CollapsibleSection";
+import Button from "../../../components/ui/Button";
+import Checkbox from "../../../components/ui/Checkbox";
+import ColourSwatches from "../../../components/ui/ColourSwatches";
+import ComboBox from "../../../components/ui/ComboBox";
+import InputBox from "../../../components/ui/InputBox";
+import darkSVG from "../assets/dark.svg";
+import lightSVG from "../assets/light.svg";
+import mutantSVG from "../assets/mutant_emoji.svg";
+import notoSVG from "../assets/noto_emoji.svg";
+import openmojiSVG from "../assets/openmoji_emoji.svg";
+import twemojiSVG from "../assets/twemoji_emoji.svg";
 
 interface Props {
-    settings: Settings;
+	settings: Settings;
 }
 
 // ! FIXME: code needs to be rewritten to fix jittering
 export function Component(props: Props) {
-    const theme = useContext(ThemeContext);
-    const { writeClipboard, openScreen } = useIntermediate();
+	const theme = useContext(ThemeContext);
+	const { writeClipboard, openScreen } = useIntermediate();
 
-    function setTheme(theme: ThemeOptions) {
-        dispatch({
-            type: "SETTINGS_SET_THEME",
-            theme
-        });
-    }
+	function setTheme(theme: ThemeOptions) {
+		dispatch({
+			type: "SETTINGS_SET_THEME",
+			theme,
+		});
+	}
 
-    function pushOverride(custom: Partial<Theme>) {
-        dispatch({
-            type: "SETTINGS_SET_THEME_OVERRIDE",
-            custom
-        });
-    }
+	function pushOverride(custom: Partial<Theme>) {
+		dispatch({
+			type: "SETTINGS_SET_THEME_OVERRIDE",
+			custom,
+		});
+	}
 
-    function setAccent(accent: string) {
-        setOverride({
-            accent,
-            "scrollbar-thumb": pSBC(-0.2, accent)
-        });
-    }
+	function setAccent(accent: string) {
+		setOverride({
+			accent,
+			"scrollbar-thumb": pSBC(-0.2, accent),
+		});
+	}
 
-    const emojiPack = props.settings.appearance?.emojiPack ?? 'mutant';
-    function setEmojiPack(emojiPack: EmojiPacks) {
-        dispatch({
-            type: 'SETTINGS_SET_APPEARANCE',
-            options: {
-                emojiPack
-            }
-        });
-    }
+	const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
+	function setEmojiPack(emojiPack: EmojiPacks) {
+		dispatch({
+			type: "SETTINGS_SET_APPEARANCE",
+			options: {
+				emojiPack,
+			},
+		});
+	}
 
-    const setOverride = useCallback(debounce(pushOverride, 200), []) as (
-        custom: Partial<Theme>
-    ) => void;
-    const [ css, setCSS ] = useState(props.settings.theme?.custom?.css ?? '');
+	const setOverride = useCallback(debounce(pushOverride, 200), []) as (
+		custom: Partial<Theme>,
+	) => void;
+	const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
 
-    useEffect(() => setOverride({ css }), [ css ]);
+	useEffect(() => setOverride({ css }), [css]);
 
-    const selected = props.settings.theme?.preset ?? "dark";
-    return (
-        <div className={styles.appearance}>
-            <h3>
-                <Text id="app.settings.pages.appearance.theme" />
-            </h3>
-            <div className={styles.themes}>
-                <div className={styles.theme}>
-                    <img
-                        src={lightSVG}
-                        data-active={selected === "light"}
-                        onClick={() =>
-                            selected !== "light" &&
-                            setTheme({ preset: "light" })
-                        } />
-                    <h4>
-                        <Text id="app.settings.pages.appearance.color.light" />
-                    </h4>
-                </div>
-                <div className={styles.theme}>
-                    <img
-                        src={darkSVG}
-                        data-active={selected === "dark"}
-                        onClick={() =>
-                            selected !== "dark" && setTheme({ preset: "dark" })
-                        } />
-                    <h4>
-                        <Text id="app.settings.pages.appearance.color.dark" />
-                    </h4>
-                </div>
-            </div>
+	const selected = props.settings.theme?.preset ?? "dark";
+	return (
+		<div className={styles.appearance}>
+			<h3>
+				<Text id="app.settings.pages.appearance.theme" />
+			</h3>
+			<div className={styles.themes}>
+				<div className={styles.theme}>
+					<img
+						src={lightSVG}
+						data-active={selected === "light"}
+						onClick={() =>
+							selected !== "light" &&
+							setTheme({ preset: "light" })
+						}
+					/>
+					<h4>
+						<Text id="app.settings.pages.appearance.color.light" />
+					</h4>
+				</div>
+				<div className={styles.theme}>
+					<img
+						src={darkSVG}
+						data-active={selected === "dark"}
+						onClick={() =>
+							selected !== "dark" && setTheme({ preset: "dark" })
+						}
+					/>
+					<h4>
+						<Text id="app.settings.pages.appearance.color.dark" />
+					</h4>
+				</div>
+			</div>
 
-            <h3>
-                <Text id="app.settings.pages.appearance.accent_selector" />
-            </h3>
-            <ColourSwatches value={theme.accent} onChange={setAccent} />
+			<h3>
+				<Text id="app.settings.pages.appearance.accent_selector" />
+			</h3>
+			<ColourSwatches value={theme.accent} onChange={setAccent} />
 
-            {/*<h3>
+			{/*<h3>
                 <Text id="app.settings.pages.appearance.message_display" />
             </h3>
             <div className={styles.display}>
@@ -132,186 +146,229 @@ export function Component(props: Props) {
                 </Radio>
             </div>*/}
 
-            <h3>
-                <Text id="app.settings.pages.appearance.font" />
-            </h3>
-            <ComboBox value={theme.font ?? DEFAULT_FONT} onChange={e => setTheme({ custom: { font: e.currentTarget.value as any } })}>
-                {
-                    FONT_KEYS
-                        .map(key =>
-                            <option value={key}>{ FONTS[key as keyof typeof FONTS].name }</option>
-                        )
-                }
-            </ComboBox>
-            <p>
-                <Checkbox checked={props.settings.theme?.ligatures === true}
-                    onChange={() => setTheme({ ligatures: !props.settings.theme?.ligatures })}
-                    description={<Text id="app.settings.pages.appearance.ligatures_desc" />}>
-                    <Text id="app.settings.pages.appearance.ligatures" />
-                </Checkbox>
-            </p>
+			<h3>
+				<Text id="app.settings.pages.appearance.font" />
+			</h3>
+			<ComboBox
+				value={theme.font ?? DEFAULT_FONT}
+				onChange={(e) =>
+					setTheme({ custom: { font: e.currentTarget.value as any } })
+				}>
+				{FONT_KEYS.map((key) => (
+					<option value={key}>
+						{FONTS[key as keyof typeof FONTS].name}
+					</option>
+				))}
+			</ComboBox>
+			<p>
+				<Checkbox
+					checked={props.settings.theme?.ligatures === true}
+					onChange={() =>
+						setTheme({
+							ligatures: !props.settings.theme?.ligatures,
+						})
+					}
+					description={
+						<Text id="app.settings.pages.appearance.ligatures_desc" />
+					}>
+					<Text id="app.settings.pages.appearance.ligatures" />
+				</Checkbox>
+			</p>
 
-            <h3>
-                <Text id="app.settings.pages.appearance.emoji_pack" />
-            </h3>
-            <div className={styles.emojiPack}>
-                <div className={styles.row}>
-                    <div>
-                        <div className={styles.button}
-                            onClick={() => setEmojiPack('mutant')}
-                            data-active={emojiPack === 'mutant'}>
-                            <img src={mutantSVG} draggable={false} />
-                        </div>
-                        <h4>Mutant Remix <a href="https://mutant.revolt.chat" target="_blank">(by Revolt)</a></h4>
-                    </div>
-                    <div>
-                        <div className={styles.button}
-                            onClick={() => setEmojiPack('twemoji')}
-                            data-active={emojiPack === 'twemoji'}>
-                            <img src={twemojiSVG} draggable={false} />
-                        </div>
-                        <h4>Twemoji</h4>
-                    </div>
-                </div>
-                <div className={styles.row}>
-                    <div>
-                        <div className={styles.button}
-                            onClick={() => setEmojiPack('openmoji')}
-                            data-active={emojiPack === 'openmoji'}>
-                            <img src={openmojiSVG} draggable={false} />
-                        </div>
-                        <h4>Openmoji</h4>
-                    </div>
-                    <div>
-                        <div className={styles.button}
-                            onClick={() => setEmojiPack('noto')}
-                            data-active={emojiPack === 'noto'}>
-                            <img src={notoSVG} draggable={false} />
-                        </div>
-                        <h4>Noto Emoji</h4>
-                    </div>
-                </div>
-            </div>
+			<h3>
+				<Text id="app.settings.pages.appearance.emoji_pack" />
+			</h3>
+			<div className={styles.emojiPack}>
+				<div className={styles.row}>
+					<div>
+						<div
+							className={styles.button}
+							onClick={() => setEmojiPack("mutant")}
+							data-active={emojiPack === "mutant"}>
+							<img src={mutantSVG} draggable={false} />
+						</div>
+						<h4>
+							Mutant Remix{" "}
+							<a
+								href="https://mutant.revolt.chat"
+								target="_blank">
+								(by Revolt)
+							</a>
+						</h4>
+					</div>
+					<div>
+						<div
+							className={styles.button}
+							onClick={() => setEmojiPack("twemoji")}
+							data-active={emojiPack === "twemoji"}>
+							<img src={twemojiSVG} draggable={false} />
+						</div>
+						<h4>Twemoji</h4>
+					</div>
+				</div>
+				<div className={styles.row}>
+					<div>
+						<div
+							className={styles.button}
+							onClick={() => setEmojiPack("openmoji")}
+							data-active={emojiPack === "openmoji"}>
+							<img src={openmojiSVG} draggable={false} />
+						</div>
+						<h4>Openmoji</h4>
+					</div>
+					<div>
+						<div
+							className={styles.button}
+							onClick={() => setEmojiPack("noto")}
+							data-active={emojiPack === "noto"}>
+							<img src={notoSVG} draggable={false} />
+						</div>
+						<h4>Noto Emoji</h4>
+					</div>
+				</div>
+			</div>
 
-            <CollapsibleSection id="settings_advanced_appearance" defaultValue={false} summary={<Text id="app.settings.pages.appearance.advanced" />}>
-                <h3>
-                    <Text id="app.settings.pages.appearance.overrides" />
-                </h3>
-                <div className={styles.actions}>
-                    <Button contrast
-                        onClick={() => setTheme({ custom: {} })}>
-                        <Text id="app.settings.pages.appearance.reset_overrides" />
-                    </Button>
-                    <Button contrast
-                        onClick={() => writeClipboard(JSON.stringify(theme))}>
-                        <Text id="app.settings.pages.appearance.export_clipboard" />
-                    </Button>
-                    <Button contrast
-                        onClick={async () => {
-                            const text = await navigator.clipboard.readText();
-                            setOverride(JSON.parse(text));
-                        }}>
-                        <Text id="app.settings.pages.appearance.import_clipboard" />
-                    </Button>
-                    <Button contrast
-                        onClick={async () => {
-                            openScreen({
-                                id: "_input",
-                                question: <Text id="app.settings.pages.appearance.import_theme" />,
-                                field: <Text id="app.settings.pages.appearance.theme_data" />,
-                                callback: async string => setOverride(JSON.parse(string))
-                            });
-                        }}>
-                        <Text id="app.settings.pages.appearance.import_manual" />
-                    </Button>
-                </div>
-                <div className={styles.overrides}>
-                    {([
-                        "accent",
-                        "background",
-                        "foreground",
-                        "primary-background",
-                        "primary-header",
-                        "secondary-background",
-                        "secondary-foreground",
-                        "secondary-header",
-                        "tertiary-background",
-                        "tertiary-foreground",
-                        "block",
-                        "message-box",
-                        "mention",
-                        "scrollbar-thumb",
-                        "scrollbar-track",
-                        "status-online",
-                        "status-away",
-                        "status-busy",
-                        "status-streaming",
-                        "status-invisible",
-                        "success",
-                        "warning",
-                        "error",
-                        "hover"
-                    ] as const).map(x => (
-                        <div className={styles.entry} key={x}>
-                            <span>{x}</span>
-                            <div className={styles.override}>
-                                <div className={styles.picker}
-                                    style={{ backgroundColor: theme[x] }}>
-                                    <input
-                                        type="color"
-                                        value={theme[x]}
-                                        onChange={v =>
-                                            setOverride({
-                                                [x]: v.currentTarget.value
-                                            })
-                                        }
-                                    />
-                                </div>
-                                <InputBox
-                                    className={styles.text}
-                                    value={theme[x]}
-                                    onChange={y =>
-                                        setOverride({
-                                            [x]: y.currentTarget.value
-                                        })
-                                    }
-                                />
-                            </div>
-                        </div>
-                    ))}
-                </div>
+			<CollapsibleSection
+				id="settings_advanced_appearance"
+				defaultValue={false}
+				summary={<Text id="app.settings.pages.appearance.advanced" />}>
+				<h3>
+					<Text id="app.settings.pages.appearance.overrides" />
+				</h3>
+				<div className={styles.actions}>
+					<Button contrast onClick={() => setTheme({ custom: {} })}>
+						<Text id="app.settings.pages.appearance.reset_overrides" />
+					</Button>
+					<Button
+						contrast
+						onClick={() => writeClipboard(JSON.stringify(theme))}>
+						<Text id="app.settings.pages.appearance.export_clipboard" />
+					</Button>
+					<Button
+						contrast
+						onClick={async () => {
+							const text = await navigator.clipboard.readText();
+							setOverride(JSON.parse(text));
+						}}>
+						<Text id="app.settings.pages.appearance.import_clipboard" />
+					</Button>
+					<Button
+						contrast
+						onClick={async () => {
+							openScreen({
+								id: "_input",
+								question: (
+									<Text id="app.settings.pages.appearance.import_theme" />
+								),
+								field: (
+									<Text id="app.settings.pages.appearance.theme_data" />
+								),
+								callback: async (string) =>
+									setOverride(JSON.parse(string)),
+							});
+						}}>
+						<Text id="app.settings.pages.appearance.import_manual" />
+					</Button>
+				</div>
+				<div className={styles.overrides}>
+					{(
+						[
+							"accent",
+							"background",
+							"foreground",
+							"primary-background",
+							"primary-header",
+							"secondary-background",
+							"secondary-foreground",
+							"secondary-header",
+							"tertiary-background",
+							"tertiary-foreground",
+							"block",
+							"message-box",
+							"mention",
+							"scrollbar-thumb",
+							"scrollbar-track",
+							"status-online",
+							"status-away",
+							"status-busy",
+							"status-streaming",
+							"status-invisible",
+							"success",
+							"warning",
+							"error",
+							"hover",
+						] as const
+					).map((x) => (
+						<div className={styles.entry} key={x}>
+							<span>{x}</span>
+							<div className={styles.override}>
+								<div
+									className={styles.picker}
+									style={{ backgroundColor: theme[x] }}>
+									<input
+										type="color"
+										value={theme[x]}
+										onChange={(v) =>
+											setOverride({
+												[x]: v.currentTarget.value,
+											})
+										}
+									/>
+								</div>
+								<InputBox
+									className={styles.text}
+									value={theme[x]}
+									onChange={(y) =>
+										setOverride({
+											[x]: y.currentTarget.value,
+										})
+									}
+								/>
+							</div>
+						</div>
+					))}
+				</div>
 
-                <h3>
-                    <Text id="app.settings.pages.appearance.mono_font" />
-                </h3>
-                <ComboBox value={theme.monoscapeFont ?? DEFAULT_MONO_FONT} onChange={e => setTheme({ custom: { monoscapeFont: e.currentTarget.value as any } })}>
-                    {
-                        MONOSCAPE_FONT_KEYS
-                            .map(key =>
-                                <option value={key}>{ MONOSCAPE_FONTS[key as keyof typeof MONOSCAPE_FONTS].name }</option>
-                            )
-                    }
-                </ComboBox>
+				<h3>
+					<Text id="app.settings.pages.appearance.mono_font" />
+				</h3>
+				<ComboBox
+					value={theme.monoscapeFont ?? DEFAULT_MONO_FONT}
+					onChange={(e) =>
+						setTheme({
+							custom: {
+								monoscapeFont: e.currentTarget.value as any,
+							},
+						})
+					}>
+					{MONOSCAPE_FONT_KEYS.map((key) => (
+						<option value={key}>
+							{
+								MONOSCAPE_FONTS[
+									key as keyof typeof MONOSCAPE_FONTS
+								].name
+							}
+						</option>
+					))}
+				</ComboBox>
 
-                <h3>
-                    <Text id="app.settings.pages.appearance.custom_css" />
-                </h3>
-                <TextAreaAutoSize
-                    maxRows={20}
-                    minHeight={480}
-                    code
-                    value={css}
-                    onChange={ev => setCSS(ev.currentTarget.value)} />
-            </CollapsibleSection>
-        </div>
-    );
+				<h3>
+					<Text id="app.settings.pages.appearance.custom_css" />
+				</h3>
+				<TextAreaAutoSize
+					maxRows={20}
+					minHeight={480}
+					code
+					value={css}
+					onChange={(ev) => setCSS(ev.currentTarget.value)}
+				/>
+			</CollapsibleSection>
+		</div>
+	);
 }
 
-export const Appearance = connectState(
-    Component,
-    state => {
-        return {
-            settings: state.settings
-        };
-    }
-);
+export const Appearance = connectState(Component, (state) => {
+	return {
+		settings: state.settings,
+	};
+});
diff --git a/src/pages/settings/panes/Experiments.tsx b/src/pages/settings/panes/Experiments.tsx
index c5dee4a9e303f48eda9334bd10aba88cfd3c6f0e..1ecf8c8e4222d10353523b9e097443ee71feecdb 100644
--- a/src/pages/settings/panes/Experiments.tsx
+++ b/src/pages/settings/panes/Experiments.tsx
@@ -1,55 +1,55 @@
-import { Text } from "preact-i18n";
 import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
+
 import { dispatch } from "../../../redux";
-import Checkbox from "../../../components/ui/Checkbox";
 import { connectState } from "../../../redux/connector";
-import { AVAILABLE_EXPERIMENTS, ExperimentOptions } from "../../../redux/reducers/experiments";
+import {
+	AVAILABLE_EXPERIMENTS,
+	ExperimentOptions,
+} from "../../../redux/reducers/experiments";
+
+import Checkbox from "../../../components/ui/Checkbox";
 
 interface Props {
-    options?: ExperimentOptions;
+	options?: ExperimentOptions;
 }
 
 export function Component(props: Props) {
-    return (
-        <div className={styles.experiments}>
-            <h3>
-                <Text id="app.settings.pages.experiments.features" />
-            </h3>
-            {
-                (AVAILABLE_EXPERIMENTS).map(
-                    key =>
-                        <Checkbox
-                            checked={(props.options?.enabled ?? []).indexOf(key) > -1}
-                            onChange={enabled =>
-                                dispatch({
-                                    type: enabled ? 'EXPERIMENTS_ENABLE' : 'EXPERIMENTS_DISABLE',
-                                    key
-                                })
-                            }
-                        >
-                            <Text id={`app.settings.pages.experiments.titles.${key}`} />
-                            <p>
-                                <Text id={`app.settings.pages.experiments.descriptions.${key}`} />
-                            </p>
-                        </Checkbox>
-                )
-            }
-            {
-                AVAILABLE_EXPERIMENTS.length === 0 &&
-                <div className={styles.empty}>
-                    <Text id="app.settings.pages.experiments.not_available" />
-                </div>
-                
-            }
-        </div>
-    );
+	return (
+		<div className={styles.experiments}>
+			<h3>
+				<Text id="app.settings.pages.experiments.features" />
+			</h3>
+			{AVAILABLE_EXPERIMENTS.map((key) => (
+				<Checkbox
+					checked={(props.options?.enabled ?? []).indexOf(key) > -1}
+					onChange={(enabled) =>
+						dispatch({
+							type: enabled
+								? "EXPERIMENTS_ENABLE"
+								: "EXPERIMENTS_DISABLE",
+							key,
+						})
+					}>
+					<Text id={`app.settings.pages.experiments.titles.${key}`} />
+					<p>
+						<Text
+							id={`app.settings.pages.experiments.descriptions.${key}`}
+						/>
+					</p>
+				</Checkbox>
+			))}
+			{AVAILABLE_EXPERIMENTS.length === 0 && (
+				<div className={styles.empty}>
+					<Text id="app.settings.pages.experiments.not_available" />
+				</div>
+			)}
+		</div>
+	);
 }
 
-export const ExperimentsPage = connectState(
-    Component,
-    state => {
-        return {
-            options: state.experiments
-        };
-    }
-);
+export const ExperimentsPage = connectState(Component, (state) => {
+	return {
+		options: state.experiments,
+	};
+});
diff --git a/src/pages/settings/panes/Feedback.tsx b/src/pages/settings/panes/Feedback.tsx
index 66d21cd692056cb77dde2a911bb783de22b7159d..fe86dd0c423d80cb42f7f26ee67391d0ce5933b6 100644
--- a/src/pages/settings/panes/Feedback.tsx
+++ b/src/pages/settings/panes/Feedback.tsx
@@ -1,106 +1,110 @@
-import { useState } from "preact/hooks";
 import styles from "./Panes.module.scss";
 import { Localizer, Text } from "preact-i18n";
-import Radio from "../../../components/ui/Radio";
+import { useState } from "preact/hooks";
+
+import { useSelf } from "../../../context/revoltjs/hooks";
+
 import Button from "../../../components/ui/Button";
 import InputBox from "../../../components/ui/InputBox";
+import Radio from "../../../components/ui/Radio";
 import TextArea from "../../../components/ui/TextArea";
-import { useSelf } from "../../../context/revoltjs/hooks";
 
 export function Feedback() {
-    const user = useSelf();
-    const [other, setOther] = useState("");
-    const [description, setDescription] = useState("");
-    const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
-    const [checked, setChecked] = useState<
-        "Bug" | "Feature Request" | "__other_option__"
-    >("Bug");
+	const user = useSelf();
+	const [other, setOther] = useState("");
+	const [description, setDescription] = useState("");
+	const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
+	const [checked, setChecked] = useState<
+		"Bug" | "Feature Request" | "__other_option__"
+	>("Bug");
 
-    async function onSubmit(ev: JSX.TargetedEvent<HTMLFormElement, Event>) {
-        ev.preventDefault();
-        setState("sending");
+	async function onSubmit(ev: JSX.TargetedEvent<HTMLFormElement, Event>) {
+		ev.preventDefault();
+		setState("sending");
 
-        await fetch(
-            `https://workers.revolt.chat/feedback`,
-            {
-                method: "POST",
-                body: JSON.stringify({
-                    checked,
-                    other,
-                    description,
-                    name: user?.username ?? "Unknown User"
-                }),
-                mode: 'no-cors'
-            }
-        );
+		await fetch(`https://workers.revolt.chat/feedback`, {
+			method: "POST",
+			body: JSON.stringify({
+				checked,
+				other,
+				description,
+				name: user?.username ?? "Unknown User",
+			}),
+			mode: "no-cors",
+		});
 
-        setState("sent");
-        setChecked("Bug");
-        setDescription("");
-        setOther("");
-    }
+		setState("sent");
+		setChecked("Bug");
+		setDescription("");
+		setOther("");
+	}
 
-    return (
-        <form className={styles.feedback} onSubmit={onSubmit}>
-            <h3>
-                <Text id="app.settings.pages.feedback.report" />
-            </h3>
-            <div className={styles.options}>
-                <Radio
-                    checked={checked === "Bug"}
-                    disabled={state === "sending"}
-                    onSelect={() => setChecked("Bug")}>
-                    <Text id="app.settings.pages.feedback.bug" />
-                </Radio>
-                <Radio
-                    disabled={state === "sending"}
-                    checked={checked === "Feature Request"}
-                    onSelect={() => setChecked("Feature Request")}>
-                    <Text id="app.settings.pages.feedback.feature" />
-                </Radio>
-                { (location.hostname === 'vite.revolt.chat' || location.hostname === 'local.revolt.chat') && <Radio
-                    disabled={state === "sending"}
-                    checked={other === "Revite"}
-                    onSelect={() => {
-                        setChecked("__other_option__");
-                        setOther("Revite");
-                    }}>
-                    Issues with Revite
-                </Radio> }
-                <Radio
-                    disabled={state === "sending"}
-                    checked={checked === "__other_option__" && other !== "Revite"}
-                    onSelect={() => setChecked("__other_option__")}>
-                    <Localizer>
-                        <InputBox
-                            value={other}
-                            disabled={state === "sending"}
-                            name="entry.1151440373.other_option_response"
-                            onChange={e => setOther(e.currentTarget.value)}
-                            placeholder={
-                                (
-                                    <Text id="app.settings.pages.feedback.other" />
-                                ) as any
-                            }
-                        />
-                    </Localizer>
-                </Radio>
-            </div>
-            <h3>
-                <Text id="app.settings.pages.feedback.describe" />
-            </h3>
-            <TextArea
-                // maxRows={10}
-                value={description}
-                id="entry.685672624"
-                disabled={state === "sending"}
-                onChange={ev => setDescription(ev.currentTarget.value)}
-            />
-            <p>
-                <Button type="submit" contrast>
-                    <Text id="app.settings.pages.feedback.send" />
-                </Button>
-            </p>
-        </form>
-    );
+	return (
+		<form className={styles.feedback} onSubmit={onSubmit}>
+			<h3>
+				<Text id="app.settings.pages.feedback.report" />
+			</h3>
+			<div className={styles.options}>
+				<Radio
+					checked={checked === "Bug"}
+					disabled={state === "sending"}
+					onSelect={() => setChecked("Bug")}>
+					<Text id="app.settings.pages.feedback.bug" />
+				</Radio>
+				<Radio
+					disabled={state === "sending"}
+					checked={checked === "Feature Request"}
+					onSelect={() => setChecked("Feature Request")}>
+					<Text id="app.settings.pages.feedback.feature" />
+				</Radio>
+				{(location.hostname === "vite.revolt.chat" ||
+					location.hostname === "local.revolt.chat") && (
+					<Radio
+						disabled={state === "sending"}
+						checked={other === "Revite"}
+						onSelect={() => {
+							setChecked("__other_option__");
+							setOther("Revite");
+						}}>
+						Issues with Revite
+					</Radio>
+				)}
+				<Radio
+					disabled={state === "sending"}
+					checked={
+						checked === "__other_option__" && other !== "Revite"
+					}
+					onSelect={() => setChecked("__other_option__")}>
+					<Localizer>
+						<InputBox
+							value={other}
+							disabled={state === "sending"}
+							name="entry.1151440373.other_option_response"
+							onChange={(e) => setOther(e.currentTarget.value)}
+							placeholder={
+								(
+									<Text id="app.settings.pages.feedback.other" />
+								) as any
+							}
+						/>
+					</Localizer>
+				</Radio>
+			</div>
+			<h3>
+				<Text id="app.settings.pages.feedback.describe" />
+			</h3>
+			<TextArea
+				// maxRows={10}
+				value={description}
+				id="entry.685672624"
+				disabled={state === "sending"}
+				onChange={(ev) => setDescription(ev.currentTarget.value)}
+			/>
+			<p>
+				<Button type="submit" contrast>
+					<Text id="app.settings.pages.feedback.send" />
+				</Button>
+			</p>
+		</form>
+	);
 }
diff --git a/src/pages/settings/panes/Languages.tsx b/src/pages/settings/panes/Languages.tsx
index 077391a592367002bf611c480acb16ba71b08369..772795c6b7db4c3cc36335902850fb7f3da27be4 100644
--- a/src/pages/settings/panes/Languages.tsx
+++ b/src/pages/settings/panes/Languages.tsx
@@ -1,84 +1,91 @@
-import { Text } from "preact-i18n";
 import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
+
 import { dispatch } from "../../../redux";
-import Tip from "../../../components/ui/Tip";
+import { connectState } from "../../../redux/connector";
+
+import {
+	Language,
+	LanguageEntry,
+	Languages as Langs,
+} from "../../../context/Locale";
+
 import Emoji from "../../../components/common/Emoji";
 import Checkbox from "../../../components/ui/Checkbox";
-import { connectState } from "../../../redux/connector";
-import { Language, LanguageEntry, Languages as Langs } from "../../../context/Locale";
+import Tip from "../../../components/ui/Tip";
 
 type Props = {
-    locale: Language;
-}
+	locale: Language;
+};
 
-type Key = [ string, LanguageEntry ];
+type Key = [string, LanguageEntry];
 
-function Entry({ entry: [ x, lang ], locale }: { entry: Key } & Props) {
-    return (
-        <Checkbox
-            key={x}
-            className={styles.entry}
-            checked={locale === x}
-            onChange={v => {
-                if (v) {
-                    dispatch({
-                        type: "SET_LOCALE",
-                        locale: x as Language
-                    });
-                }
-            }}
-        >
-            <div className={styles.flag}><Emoji size={42} emoji={lang.emoji} /></div>
-            <span className={styles.description}>
-                {lang.display}
-            </span>
-        </Checkbox>
-    );
+function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
+	return (
+		<Checkbox
+			key={x}
+			className={styles.entry}
+			checked={locale === x}
+			onChange={(v) => {
+				if (v) {
+					dispatch({
+						type: "SET_LOCALE",
+						locale: x as Language,
+					});
+				}
+			}}>
+			<div className={styles.flag}>
+				<Emoji size={42} emoji={lang.emoji} />
+			</div>
+			<span className={styles.description}>{lang.display}</span>
+		</Checkbox>
+	);
 }
 
 export function Component(props: Props) {
-    const languages = Object
-        .keys(Langs)
-        .map(x => [ x, Langs[x as keyof typeof Langs] ]) as Key[];
+	const languages = Object.keys(Langs).map((x) => [
+		x,
+		Langs[x as keyof typeof Langs],
+	]) as Key[];
 
-    return (
-        <div className={styles.languages}>
-            <h3>
-                <Text id="app.settings.pages.language.select" />
-            </h3>
-            <div className={styles.list}>
-                {languages
-                    .filter(([, lang]) => !lang.alt)
-                    .map(([x, lang]) => <Entry key={x} entry={[x, lang]} {...props} />)}
-            </div>
-            <h3>
-                <Text id="app.settings.pages.language.other" />
-            </h3>
-            <div className={styles.list}>
-                {languages
-                    .filter(([, lang]) => lang.alt)
-                    .map(([x, lang]) => <Entry key={x} entry={[x, lang]} {...props} />)}
-            </div>
-            <Tip>
-                <span>
-                    <Text id="app.settings.tips.languages.a" />
-                </span>{" "}
-                <a
-                    href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
-                    target="_blank"
-                >
-                    <Text id="app.settings.tips.languages.b" />
-                </a>
-            </Tip>
-        </div>
-    );
+	return (
+		<div className={styles.languages}>
+			<h3>
+				<Text id="app.settings.pages.language.select" />
+			</h3>
+			<div className={styles.list}>
+				{languages
+					.filter(([, lang]) => !lang.alt)
+					.map(([x, lang]) => (
+						<Entry key={x} entry={[x, lang]} {...props} />
+					))}
+			</div>
+			<h3>
+				<Text id="app.settings.pages.language.other" />
+			</h3>
+			<div className={styles.list}>
+				{languages
+					.filter(([, lang]) => lang.alt)
+					.map(([x, lang]) => (
+						<Entry key={x} entry={[x, lang]} {...props} />
+					))}
+			</div>
+			<Tip>
+				<span>
+					<Text id="app.settings.tips.languages.a" />
+				</span>{" "}
+				<a
+					href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
+					target="_blank">
+					<Text id="app.settings.tips.languages.b" />
+				</a>
+			</Tip>
+		</div>
+	);
 }
 
-export const Languages = connectState(
-    Component,
-    state => {
-        return {
-            locale: state.locale
-        };
-    }
-);
+export const Languages = connectState(Component, (state) => {
+	return {
+		locale: state.locale,
+	};
+});
diff --git a/src/pages/settings/panes/Notifications.tsx b/src/pages/settings/panes/Notifications.tsx
index 5098c85e9266c9291ea5b9c981e0ce9757c32aee..af26673815dffe4a72d33a587d1d6638fcb59bfe 100644
--- a/src/pages/settings/panes/Notifications.tsx
+++ b/src/pages/settings/panes/Notifications.tsx
@@ -1,135 +1,154 @@
-import { Text } from "preact-i18n";
-import styles from "./Panes.module.scss";
-import { dispatch } from "../../../redux";
 import defaultsDeep from "lodash.defaultsdeep";
-import Checkbox from "../../../components/ui/Checkbox";
-import { connectState } from "../../../redux/connector";
-import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
+
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
+
 import { urlBase64ToUint8Array } from "../../../lib/conversion";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import { dispatch } from "../../../redux";
+import { connectState } from "../../../redux/connector";
+import {
+	DEFAULT_SOUNDS,
+	NotificationOptions,
+	SoundOptions,
+} from "../../../redux/reducers/settings";
+
 import { useIntermediate } from "../../../context/intermediate/Intermediate";
-import { DEFAULT_SOUNDS, NotificationOptions, SoundOptions } from "../../../redux/reducers/settings";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import Checkbox from "../../../components/ui/Checkbox";
+
+import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
 
 interface Props {
-    options?: NotificationOptions;
+	options?: NotificationOptions;
 }
 
 export function Component({ options }: Props) {
-    const client = useContext(AppContext);
-    const { openScreen } = useIntermediate();
-    const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
-        undefined
-    );
+	const client = useContext(AppContext);
+	const { openScreen } = useIntermediate();
+	const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
+		undefined,
+	);
 
-    // Load current state of pushManager.
-    useEffect(() => {
-        navigator.serviceWorker?.getRegistration().then(async registration => {
-            const sub = await registration?.pushManager?.getSubscription();
-            setPushEnabled(sub !== null && sub !== undefined);
-        });
-    }, []);
+	// Load current state of pushManager.
+	useEffect(() => {
+		navigator.serviceWorker
+			?.getRegistration()
+			.then(async (registration) => {
+				const sub = await registration?.pushManager?.getSubscription();
+				setPushEnabled(sub !== null && sub !== undefined);
+			});
+	}, []);
 
-    const enabledSounds: SoundOptions = defaultsDeep(options?.sounds ?? {}, DEFAULT_SOUNDS);
-    return (
-        <div className={styles.notifications}>
-            <h3>
-                <Text id="app.settings.pages.notifications.push_notifications" />
-            </h3>
-            <Checkbox
-                disabled={!("Notification" in window)}
-                checked={options?.desktopEnabled ?? false}
-                description={<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />}
-                onChange={async desktopEnabled => {
-                    if (desktopEnabled) {
-                        let permission = await Notification.requestPermission();
-                        if (permission !== "granted") {
-                            return openScreen({
-                                id: "error",
-                                error: "DeniedNotification"
-                            });
-                        }
-                    }
+	const enabledSounds: SoundOptions = defaultsDeep(
+		options?.sounds ?? {},
+		DEFAULT_SOUNDS,
+	);
+	return (
+		<div className={styles.notifications}>
+			<h3>
+				<Text id="app.settings.pages.notifications.push_notifications" />
+			</h3>
+			<Checkbox
+				disabled={!("Notification" in window)}
+				checked={options?.desktopEnabled ?? false}
+				description={
+					<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
+				}
+				onChange={async (desktopEnabled) => {
+					if (desktopEnabled) {
+						let permission = await Notification.requestPermission();
+						if (permission !== "granted") {
+							return openScreen({
+								id: "error",
+								error: "DeniedNotification",
+							});
+						}
+					}
 
-                    dispatch({
-                        type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
-                        options: { desktopEnabled }
-                    });
-                }}
-            >
-                <Text id="app.settings.pages.notifications.enable_desktop" />
-            </Checkbox>
-            <Checkbox
-                disabled={typeof pushEnabled === "undefined"}
-                checked={pushEnabled ?? false}
-                description={<Text id="app.settings.pages.notifications.descriptions.enable_push" />}
-                onChange={async pushEnabled => {
-                    try {
-                        const reg = await navigator.serviceWorker?.getRegistration();
-                        if (reg) {
-                            if (pushEnabled) {
-                                const sub = await reg.pushManager.subscribe({
-                                    userVisibleOnly: true,
-                                    applicationServerKey: urlBase64ToUint8Array(
-                                        client.configuration!.vapid
-                                    )
-                                });
+					dispatch({
+						type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
+						options: { desktopEnabled },
+					});
+				}}>
+				<Text id="app.settings.pages.notifications.enable_desktop" />
+			</Checkbox>
+			<Checkbox
+				disabled={typeof pushEnabled === "undefined"}
+				checked={pushEnabled ?? false}
+				description={
+					<Text id="app.settings.pages.notifications.descriptions.enable_push" />
+				}
+				onChange={async (pushEnabled) => {
+					try {
+						const reg =
+							await navigator.serviceWorker?.getRegistration();
+						if (reg) {
+							if (pushEnabled) {
+								const sub = await reg.pushManager.subscribe({
+									userVisibleOnly: true,
+									applicationServerKey: urlBase64ToUint8Array(
+										client.configuration!.vapid,
+									),
+								});
 
-                                // tell the server we just subscribed
-                                const json = sub.toJSON();
-                                if (json.keys) {;
-                                    client.req("POST", "/push/subscribe", {
-                                        endpoint: sub.endpoint,
-                                        ...(json.keys as { p256dh: string, auth: string })
-                                    });
-                                    setPushEnabled(true);
-                                }
-                            } else {
-                                const sub = await reg.pushManager.getSubscription();
-                                sub?.unsubscribe();
-                                setPushEnabled(false);
+								// tell the server we just subscribed
+								const json = sub.toJSON();
+								if (json.keys) {
+									client.req("POST", "/push/subscribe", {
+										endpoint: sub.endpoint,
+										...(json.keys as {
+											p256dh: string;
+											auth: string;
+										}),
+									});
+									setPushEnabled(true);
+								}
+							} else {
+								const sub =
+									await reg.pushManager.getSubscription();
+								sub?.unsubscribe();
+								setPushEnabled(false);
 
-                                client.req("POST", "/push/unsubscribe");
-                            }
-                        }
-                    } catch (err) {
-                        console.error('Failed to enable push!', err);
-                    }
-                }}
-            >
-                <Text id="app.settings.pages.notifications.enable_push" />
-            </Checkbox>
-            <h3>
-                <Text id="app.settings.pages.notifications.sounds" />
-            </h3>
-            {
-                SOUNDS_ARRAY.map(key =>
-                    <Checkbox
-                        checked={enabledSounds[key] ? true : false}
-                        onChange={enabled =>
-                            dispatch({
-                                type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
-                                options: {
-                                    sounds: {
-                                        ...options?.sounds,
-                                        [key]: enabled
-                                    }
-                                }
-                            })
-                        }>
-                        <Text id={`app.settings.pages.notifications.sound.${key}`} />
-                    </Checkbox>
-                )
-            }
-        </div>
-    );
+								client.req("POST", "/push/unsubscribe");
+							}
+						}
+					} catch (err) {
+						console.error("Failed to enable push!", err);
+					}
+				}}>
+				<Text id="app.settings.pages.notifications.enable_push" />
+			</Checkbox>
+			<h3>
+				<Text id="app.settings.pages.notifications.sounds" />
+			</h3>
+			{SOUNDS_ARRAY.map((key) => (
+				<Checkbox
+					checked={enabledSounds[key] ? true : false}
+					onChange={(enabled) =>
+						dispatch({
+							type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
+							options: {
+								sounds: {
+									...options?.sounds,
+									[key]: enabled,
+								},
+							},
+						})
+					}>
+					<Text
+						id={`app.settings.pages.notifications.sound.${key}`}
+					/>
+				</Checkbox>
+			))}
+		</div>
+	);
 }
 
-export const Notifications = connectState(
-    Component,
-    state => {
-        return {
-            options: state.settings.notification
-        };
-    }
-);
+export const Notifications = connectState(Component, (state) => {
+	return {
+		options: state.settings.notification,
+	};
+});
diff --git a/src/pages/settings/panes/Profile.tsx b/src/pages/settings/panes/Profile.tsx
index 1b6fa429e0a61b1a9c0679033aad07dd6a367e13..6e1fd4d2394017f8e287f874e4b2ef39eaf6c3d9 100644
--- a/src/pages/settings/panes/Profile.tsx
+++ b/src/pages/settings/panes/Profile.tsx
@@ -1,143 +1,186 @@
-import styles from "./Panes.module.scss";
-import Button from "../../../components/ui/Button";
 import { Users } from "revolt.js/dist/api/objects";
+
+import styles from "./Panes.module.scss";
 import { IntlContext, Text, translate } from "preact-i18n";
-import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
 import { useContext, useEffect, useState } from "preact/hooks";
+
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
+
+import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
 import { FileUploader } from "../../../context/revoltjs/FileUploads";
+import {
+	ClientStatus,
+	StatusContext,
+} from "../../../context/revoltjs/RevoltClient";
 import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
-import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
-import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
-import AutoComplete, { useAutoComplete } from "../../../components/common/AutoComplete";
+
+import AutoComplete, {
+	useAutoComplete,
+} from "../../../components/common/AutoComplete";
+import Button from "../../../components/ui/Button";
 
 export function Profile() {
-    const { intl } = useContext(IntlContext);
-    const status = useContext(StatusContext);
+	const { intl } = useContext(IntlContext);
+	const status = useContext(StatusContext);
 
-    const ctx = useForceUpdate();
-    const user = useSelf();
-    if (!user) return null;
+	const ctx = useForceUpdate();
+	const user = useSelf();
+	if (!user) return null;
 
-    const [profile, setProfile] = useState<undefined | Users.Profile>(
-        undefined
-    );
+	const [profile, setProfile] = useState<undefined | Users.Profile>(
+		undefined,
+	);
 
-    // ! FIXME: temporary solution
-    // ! we should just announce profile changes through WS
-    function refreshProfile() {
-        ctx.client.users
-            .fetchProfile(user!._id)
-            .then(profile => setProfile(profile ?? {}));
-    }
+	// ! FIXME: temporary solution
+	// ! we should just announce profile changes through WS
+	function refreshProfile() {
+		ctx.client.users
+			.fetchProfile(user!._id)
+			.then((profile) => setProfile(profile ?? {}));
+	}
 
-    useEffect(() => {
-        if (profile === undefined && status === ClientStatus.ONLINE) {
-            refreshProfile();
-        }
-    }, [status]);
+	useEffect(() => {
+		if (profile === undefined && status === ClientStatus.ONLINE) {
+			refreshProfile();
+		}
+	}, [status]);
 
-    const [ changed, setChanged ] = useState(false);
-    function setContent(content?: string) {
-        setProfile({ ...profile, content })
-        if (!changed) setChanged(true)
-    }
+	const [changed, setChanged] = useState(false);
+	function setContent(content?: string) {
+		setProfile({ ...profile, content });
+		if (!changed) setChanged(true);
+	}
 
-    const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } =
-        useAutoComplete(setContent, {
-            users: { type: 'all' }
-        });
+	const {
+		onChange,
+		onKeyUp,
+		onKeyDown,
+		onFocus,
+		onBlur,
+		...autoCompleteProps
+	} = useAutoComplete(setContent, {
+		users: { type: "all" },
+	});
 
-    return (
-        <div className={styles.user}>
-            <h3>
-                <Text id="app.special.modals.actions.preview" />
-            </h3>
-            <div className={styles.preview}>
-                <UserProfile
-                    user_id={user._id}
-                    dummy={true}
-                    dummyProfile={profile}
-                    onClose={() => {}}
-                />
-            </div>
-            <div className={styles.row}>
-                <div className={styles.pfp}>
-                    <h3>
-                        <Text id="app.settings.pages.profile.profile_picture" />
-                    </h3>
-                    <FileUploader
-                        width={80}
-                        height={80}
-                        style="icon"
-                        fileType="avatars"
-                        behaviour="upload"
-                        maxFileSize={4_000_000}
-                        onUpload={avatar => ctx.client.users.editUser({ avatar })}
-                        remove={() => ctx.client.users.editUser({ remove: 'Avatar' })}
-                        defaultPreview={ctx.client.users.getAvatarURL(user._id, { max_side: 256 }, true)}
-                        previewURL={ctx.client.users.getAvatarURL(user._id, { max_side: 256 }, true, true)}
-                    />
-                </div>
-                <div className={styles.background}>
-                    <h3>
-                        <Text id="app.settings.pages.profile.custom_background" />
-                    </h3>
-                    <FileUploader
-                        height={92}
-                        style="banner"
-                        behaviour="upload"
-                        fileType="backgrounds"
-                        maxFileSize={6_000_000}
-                        onUpload={async background => {
-                            await ctx.client.users.editUser({ profile: { background } });
-                            refreshProfile();
-                        }}
-                        remove={async () => {
-                            await ctx.client.users.editUser({ remove: 'ProfileBackground' });
-                            setProfile({ ...profile, background: undefined });
-                        }}
-                        previewURL={profile?.background ? ctx.client.users.getBackgroundURL(profile, { width: 1000 }, true) : undefined}
-                    />
-                </div>
-            </div>
-            <h3>
-                <Text id="app.settings.pages.profile.info" />
-            </h3>
-            <AutoComplete detached {...autoCompleteProps} />
-            <TextAreaAutoSize
-                maxRows={10}
-                minHeight={200}
-                maxLength={2000}
-                value={profile?.content ?? ""}
-                disabled={typeof profile === "undefined"}
-                onChange={ev => {
-                    onChange(ev);
-                    setContent(ev.currentTarget.value)
-                }}
-                placeholder={translate(
-                    `app.settings.pages.profile.${
-                        typeof profile === "undefined"
-                            ? "fetching"
-                            : "placeholder"
-                    }`,
-                    "",
-                    (intl as any).dictionary as Record<string, unknown>
-                )}
-                onKeyUp={onKeyUp}
-                onKeyDown={onKeyDown}
-                onFocus={onFocus}
-                onBlur={onBlur}
-            />
-            <p>
-                <Button contrast
-                    onClick={() => {
-                        setChanged(false);
-                        ctx.client.users.editUser({ profile: { content: profile?.content } })
-                    }}
-                    disabled={!changed}>
-                    <Text id="app.special.modals.actions.save" />
-                </Button>
-            </p>
-        </div>
-    );
+	return (
+		<div className={styles.user}>
+			<h3>
+				<Text id="app.special.modals.actions.preview" />
+			</h3>
+			<div className={styles.preview}>
+				<UserProfile
+					user_id={user._id}
+					dummy={true}
+					dummyProfile={profile}
+					onClose={() => {}}
+				/>
+			</div>
+			<div className={styles.row}>
+				<div className={styles.pfp}>
+					<h3>
+						<Text id="app.settings.pages.profile.profile_picture" />
+					</h3>
+					<FileUploader
+						width={80}
+						height={80}
+						style="icon"
+						fileType="avatars"
+						behaviour="upload"
+						maxFileSize={4_000_000}
+						onUpload={(avatar) =>
+							ctx.client.users.editUser({ avatar })
+						}
+						remove={() =>
+							ctx.client.users.editUser({ remove: "Avatar" })
+						}
+						defaultPreview={ctx.client.users.getAvatarURL(
+							user._id,
+							{ max_side: 256 },
+							true,
+						)}
+						previewURL={ctx.client.users.getAvatarURL(
+							user._id,
+							{ max_side: 256 },
+							true,
+							true,
+						)}
+					/>
+				</div>
+				<div className={styles.background}>
+					<h3>
+						<Text id="app.settings.pages.profile.custom_background" />
+					</h3>
+					<FileUploader
+						height={92}
+						style="banner"
+						behaviour="upload"
+						fileType="backgrounds"
+						maxFileSize={6_000_000}
+						onUpload={async (background) => {
+							await ctx.client.users.editUser({
+								profile: { background },
+							});
+							refreshProfile();
+						}}
+						remove={async () => {
+							await ctx.client.users.editUser({
+								remove: "ProfileBackground",
+							});
+							setProfile({ ...profile, background: undefined });
+						}}
+						previewURL={
+							profile?.background
+								? ctx.client.users.getBackgroundURL(
+										profile,
+										{ width: 1000 },
+										true,
+								  )
+								: undefined
+						}
+					/>
+				</div>
+			</div>
+			<h3>
+				<Text id="app.settings.pages.profile.info" />
+			</h3>
+			<AutoComplete detached {...autoCompleteProps} />
+			<TextAreaAutoSize
+				maxRows={10}
+				minHeight={200}
+				maxLength={2000}
+				value={profile?.content ?? ""}
+				disabled={typeof profile === "undefined"}
+				onChange={(ev) => {
+					onChange(ev);
+					setContent(ev.currentTarget.value);
+				}}
+				placeholder={translate(
+					`app.settings.pages.profile.${
+						typeof profile === "undefined"
+							? "fetching"
+							: "placeholder"
+					}`,
+					"",
+					(intl as any).dictionary as Record<string, unknown>,
+				)}
+				onKeyUp={onKeyUp}
+				onKeyDown={onKeyDown}
+				onFocus={onFocus}
+				onBlur={onBlur}
+			/>
+			<p>
+				<Button
+					contrast
+					onClick={() => {
+						setChanged(false);
+						ctx.client.users.editUser({
+							profile: { content: profile?.content },
+						});
+					}}
+					disabled={!changed}>
+					<Text id="app.special.modals.actions.save" />
+				</Button>
+			</p>
+		</div>
+	);
 }
diff --git a/src/pages/settings/panes/Sessions.tsx b/src/pages/settings/panes/Sessions.tsx
index 2ec357be7e352aea5d0ad1676eaa554f2b29e7d6..38a85576f1ba68940a1cc73f2694a634641306cc 100644
--- a/src/pages/settings/panes/Sessions.tsx
+++ b/src/pages/settings/panes/Sessions.tsx
@@ -1,186 +1,186 @@
+import { HelpCircle } from "@styled-icons/boxicons-regular";
+import {
+	Android,
+	Firefoxbrowser,
+	Googlechrome,
+	Ios,
+	Linux,
+	Macos,
+	Microsoftedge,
+	Safari,
+	Windows,
+} from "@styled-icons/simple-icons";
 import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { useHistory } from "react-router-dom";
 import { decodeTime } from "ulid";
-import { Text } from "preact-i18n";
+
 import styles from "./Panes.module.scss";
-import Tip from "../../../components/ui/Tip";
-import { useHistory } from "react-router-dom";
-import Button from "../../../components/ui/Button";
-import Preloader from "../../../components/ui/Preloader";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
+
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 
-import { HelpCircle } from "@styled-icons/boxicons-regular";
-import {
-    Android,
-    Firefoxbrowser,
-    Googlechrome,
-    Ios,
-    Linux,
-    Macos,
-    Microsoftedge,
-    Safari,
-    Windows
-} from "@styled-icons/simple-icons";
+import Button from "../../../components/ui/Button";
+import Preloader from "../../../components/ui/Preloader";
+import Tip from "../../../components/ui/Tip";
 
-import relativeTime from "dayjs/plugin/relativeTime";
 dayjs.extend(relativeTime);
 
 interface Session {
-    id: string;
-    friendly_name: string;
+	id: string;
+	friendly_name: string;
 }
 
 export function Sessions() {
-    const client = useContext(AppContext);
-    const deviceId = client.session?.id;
-
-    const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
-    const [attemptingDelete, setDelete] = useState<string[]>([]);
-    const history = useHistory();
-
-    function switchPage(to: string) {
-        history.replace(`/settings/${to}`);
-    }
-
-    useEffect(() => {
-        client.req("GET", "/auth/sessions").then(data => {
-            data.sort(
-                (a, b) =>
-                    (b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0)
-            );
-            setSessions(data);
-        });
-    }, []);
-
-    if (typeof sessions === "undefined") {
-        return (
-            <div className={styles.loader}>
-                <Preloader type="ring" />
-            </div>
-        );
-    }
-
-    function getIcon(session: Session) {
-        const name = session.friendly_name;
-        switch (true) {
-            case /firefox/i.test(name):
-                return <Firefoxbrowser />;
-            case /chrome/i.test(name):
-                return <Googlechrome />;
-            case /safari/i.test(name):
-                return <Safari />;
-            case /edge/i.test(name):
-                return <Microsoftedge />;
-            default:
-                return <HelpCircle />;
-        }
-    }
-
-    function getSystemIcon(session: Session) {
-        const name = session.friendly_name;
-        switch (true) {
-            case /linux/i.test(name):
-                return <Linux />;
-            case /android/i.test(name):
-                return <Android />;
-            case /mac.*os/i.test(name):
-                return <Macos />;
-            case /ios/i.test(name):
-                return <Ios />;
-            case /windows/i.test(name):
-                return <Windows />;
-            default:
-                return null;
-        }
-    }
-
-    const mapped = sessions.map(session => {
-        return {
-            ...session,
-            timestamp: decodeTime(session.id)
-        };
-    });
-
-    mapped.sort((a, b) => b.timestamp - a.timestamp);
-    let id = mapped.findIndex(x => x.id === deviceId);
-
-    const render = [
-        mapped[id],
-        ...mapped.slice(0, id),
-        ...mapped.slice(id + 1, mapped.length)
-    ];
-
-    return (
-        <div className={styles.sessions}>
-            <h3>
-                <Text id="app.settings.pages.sessions.active_sessions" />
-            </h3>
-            {render.map(session => (
-                <div
-                    className={styles.entry}
-                    data-active={session.id === deviceId}
-                    data-deleting={attemptingDelete.indexOf(session.id) > -1}
-                >
-                    {deviceId === session.id && (
-                        <span className={styles.label}>
-                            <Text id="app.settings.pages.sessions.this_device" />{" "}
-                        </span>
-                    )}
-                    <div className={styles.session}>
-                        <div className={styles.icon}>
-                            {getIcon(session)}
-                            <div>{getSystemIcon(session)}</div>
-                        </div>
-                        <div className={styles.info}>
-                            <span className={styles.name}>
-                                {session.friendly_name}
-                            </span>
-                            <span className={styles.time}>
-                                <Text
-                                    id="app.settings.pages.sessions.created"
-                                    fields={{
-                                        time_ago: dayjs(
-                                            session.timestamp
-                                        ).fromNow()
-                                    }}
-                                />
-                            </span>
-                        </div>
-                        {deviceId !== session.id && (
-                            <Button
-                                onClick={async () => {
-                                    setDelete([
-                                        ...attemptingDelete,
-                                        session.id
-                                    ]);
-                                    await client.req(
-                                        "DELETE",
-                                        `/auth/sessions/${session.id}` as '/auth/sessions'
-                                    );
-                                    setSessions(
-                                        sessions?.filter(
-                                            x => x.id !== session.id
-                                        )
-                                    );
-                                }}
-                                disabled={
-                                    attemptingDelete.indexOf(session.id) > -1
-                                }
-                            >
-                                <Text id="app.settings.pages.logOut" />
-                            </Button>
-                        )}
-                    </div>
-                </div>
-            ))}
-            <Tip>
-                <span>
-                    <Text id="app.settings.tips.sessions.a" />
-                </span>{" "}
-                <a onClick={() => switchPage("account")}>
-                    <Text id="app.settings.tips.sessions.b" />
-                </a>
-            </Tip>
-        </div>
-    );
+	const client = useContext(AppContext);
+	const deviceId = client.session?.id;
+
+	const [sessions, setSessions] = useState<Session[] | undefined>(undefined);
+	const [attemptingDelete, setDelete] = useState<string[]>([]);
+	const history = useHistory();
+
+	function switchPage(to: string) {
+		history.replace(`/settings/${to}`);
+	}
+
+	useEffect(() => {
+		client.req("GET", "/auth/sessions").then((data) => {
+			data.sort(
+				(a, b) =>
+					(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
+			);
+			setSessions(data);
+		});
+	}, []);
+
+	if (typeof sessions === "undefined") {
+		return (
+			<div className={styles.loader}>
+				<Preloader type="ring" />
+			</div>
+		);
+	}
+
+	function getIcon(session: Session) {
+		const name = session.friendly_name;
+		switch (true) {
+			case /firefox/i.test(name):
+				return <Firefoxbrowser />;
+			case /chrome/i.test(name):
+				return <Googlechrome />;
+			case /safari/i.test(name):
+				return <Safari />;
+			case /edge/i.test(name):
+				return <Microsoftedge />;
+			default:
+				return <HelpCircle />;
+		}
+	}
+
+	function getSystemIcon(session: Session) {
+		const name = session.friendly_name;
+		switch (true) {
+			case /linux/i.test(name):
+				return <Linux />;
+			case /android/i.test(name):
+				return <Android />;
+			case /mac.*os/i.test(name):
+				return <Macos />;
+			case /ios/i.test(name):
+				return <Ios />;
+			case /windows/i.test(name):
+				return <Windows />;
+			default:
+				return null;
+		}
+	}
+
+	const mapped = sessions.map((session) => {
+		return {
+			...session,
+			timestamp: decodeTime(session.id),
+		};
+	});
+
+	mapped.sort((a, b) => b.timestamp - a.timestamp);
+	let id = mapped.findIndex((x) => x.id === deviceId);
+
+	const render = [
+		mapped[id],
+		...mapped.slice(0, id),
+		...mapped.slice(id + 1, mapped.length),
+	];
+
+	return (
+		<div className={styles.sessions}>
+			<h3>
+				<Text id="app.settings.pages.sessions.active_sessions" />
+			</h3>
+			{render.map((session) => (
+				<div
+					className={styles.entry}
+					data-active={session.id === deviceId}
+					data-deleting={attemptingDelete.indexOf(session.id) > -1}>
+					{deviceId === session.id && (
+						<span className={styles.label}>
+							<Text id="app.settings.pages.sessions.this_device" />{" "}
+						</span>
+					)}
+					<div className={styles.session}>
+						<div className={styles.icon}>
+							{getIcon(session)}
+							<div>{getSystemIcon(session)}</div>
+						</div>
+						<div className={styles.info}>
+							<span className={styles.name}>
+								{session.friendly_name}
+							</span>
+							<span className={styles.time}>
+								<Text
+									id="app.settings.pages.sessions.created"
+									fields={{
+										time_ago: dayjs(
+											session.timestamp,
+										).fromNow(),
+									}}
+								/>
+							</span>
+						</div>
+						{deviceId !== session.id && (
+							<Button
+								onClick={async () => {
+									setDelete([
+										...attemptingDelete,
+										session.id,
+									]);
+									await client.req(
+										"DELETE",
+										`/auth/sessions/${session.id}` as "/auth/sessions",
+									);
+									setSessions(
+										sessions?.filter(
+											(x) => x.id !== session.id,
+										),
+									);
+								}}
+								disabled={
+									attemptingDelete.indexOf(session.id) > -1
+								}>
+								<Text id="app.settings.pages.logOut" />
+							</Button>
+						)}
+					</div>
+				</div>
+			))}
+			<Tip>
+				<span>
+					<Text id="app.settings.tips.sessions.a" />
+				</span>{" "}
+				<a onClick={() => switchPage("account")}>
+					<Text id="app.settings.tips.sessions.b" />
+				</a>
+			</Tip>
+		</div>
+	);
 }
diff --git a/src/pages/settings/panes/Sync.tsx b/src/pages/settings/panes/Sync.tsx
index 20d51136818b1852067c9c0967bb77991bcceaef..fec2548deb3d281cf11567d330bb5db18a4c3743 100644
--- a/src/pages/settings/panes/Sync.tsx
+++ b/src/pages/settings/panes/Sync.tsx
@@ -1,51 +1,56 @@
-import { Text } from "preact-i18n";
 import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
+
 import { dispatch } from "../../../redux";
-import Checkbox from "../../../components/ui/Checkbox";
 import { connectState } from "../../../redux/connector";
 import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
 
+import Checkbox from "../../../components/ui/Checkbox";
+
 interface Props {
-    options?: SyncOptions;
+	options?: SyncOptions;
 }
 
 export function Component(props: Props) {
-    return (
-        <div className={styles.notifications}>
-            <h3>
-                <Text id="app.settings.pages.sync.categories" />
-            </h3>
-            {
-                ([
-                    ['appearance', 'appearance.title'],
-                    ['theme', 'appearance.theme'],
-                    ['locale', 'language.title']
-                    // notifications sync is always-on
-                ] as [ SyncKeys, string ][]).map(
-                    ([ key, title ]) =>
-                        <Checkbox
-                            checked={(props.options?.disabled ?? []).indexOf(key) === -1}
-                            description={<Text id={`app.settings.pages.sync.descriptions.${key}`} />}
-                            onChange={enabled =>
-                                dispatch({
-                                    type: enabled ? 'SYNC_ENABLE_KEY' : 'SYNC_DISABLE_KEY',
-                                    key
-                                })
-                            }
-                        >
-                            <Text id={`app.settings.pages.${title}`} />
-                        </Checkbox>
-                )
-            }
-        </div>
-    );
+	return (
+		<div className={styles.notifications}>
+			<h3>
+				<Text id="app.settings.pages.sync.categories" />
+			</h3>
+			{(
+				[
+					["appearance", "appearance.title"],
+					["theme", "appearance.theme"],
+					["locale", "language.title"],
+					// notifications sync is always-on
+				] as [SyncKeys, string][]
+			).map(([key, title]) => (
+				<Checkbox
+					checked={
+						(props.options?.disabled ?? []).indexOf(key) === -1
+					}
+					description={
+						<Text
+							id={`app.settings.pages.sync.descriptions.${key}`}
+						/>
+					}
+					onChange={(enabled) =>
+						dispatch({
+							type: enabled
+								? "SYNC_ENABLE_KEY"
+								: "SYNC_DISABLE_KEY",
+							key,
+						})
+					}>
+					<Text id={`app.settings.pages.${title}`} />
+				</Checkbox>
+			))}
+		</div>
+	);
 }
 
-export const Sync = connectState(
-    Component,
-    state => {
-        return {
-            options: state.sync
-        };
-    }
-);
+export const Sync = connectState(Component, (state) => {
+	return {
+		options: state.sync,
+	};
+});
diff --git a/src/pages/settings/server/Bans.tsx b/src/pages/settings/server/Bans.tsx
index 5ec7c0f0581a3e0eb0e9ba7b2a46053474b0f1d0..1c92eb81f7c352ee0bcd734341df82b169a1a996 100644
--- a/src/pages/settings/server/Bans.tsx
+++ b/src/pages/settings/server/Bans.tsx
@@ -1,25 +1,37 @@
-import Tip from "../../../components/ui/Tip";
 import { Servers } from "revolt.js/dist/api/objects";
+
 import { useContext, useEffect, useState } from "preact/hooks";
+
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
 
+import Tip from "../../../components/ui/Tip";
+
 interface Props {
-    server: Servers.Server;
+	server: Servers.Server;
 }
 
 export function Bans({ server }: Props) {
-    const client = useContext(AppContext);
-    const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
+	const client = useContext(AppContext);
+	const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
 
-    useEffect(() => {
-        client.servers.fetchBans(server._id)
-            .then(bans => setBans(bans))
-    }, [ ]);
+	useEffect(() => {
+		client.servers.fetchBans(server._id).then((bans) => setBans(bans));
+	}, []);
 
-    return (
-        <div>
-            <Tip warning>This section is under construction.</Tip>
-            { bans?.map(x => <div>{x._id.user}: {x.reason ?? 'no reason'} <button onClick={() => client.servers.unbanUser(server._id, x._id.user)}>unban</button></div>) }
-        </div>
-    );
+	return (
+		<div>
+			<Tip warning>This section is under construction.</Tip>
+			{bans?.map((x) => (
+				<div>
+					{x._id.user}: {x.reason ?? "no reason"}{" "}
+					<button
+						onClick={() =>
+							client.servers.unbanUser(server._id, x._id.user)
+						}>
+						unban
+					</button>
+				</div>
+			))}
+		</div>
+	);
 }
diff --git a/src/pages/settings/server/Invites.tsx b/src/pages/settings/server/Invites.tsx
index a80c07c342ac0b0694966de8491fb1fd3fcba0e0..31e9fdd4e5a502cfc817ff946d34e879eae5935f 100644
--- a/src/pages/settings/server/Invites.tsx
+++ b/src/pages/settings/server/Invites.tsx
@@ -1,77 +1,86 @@
-import { Text } from "preact-i18n";
-import styles from './Panes.module.scss';
 import { XCircle } from "@styled-icons/boxicons-regular";
+import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects";
+
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
 import { useEffect, useState } from "preact/hooks";
-import Preloader from "../../../components/ui/Preloader";
-import IconButton from "../../../components/ui/IconButton";
-import UserIcon from "../../../components/common/user/UserIcon";
+
+import {
+	useChannels,
+	useForceUpdate,
+	useUsers,
+} from "../../../context/revoltjs/hooks";
 import { getChannelName } from "../../../context/revoltjs/util";
-import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects";
-import { useChannels, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
+
+import UserIcon from "../../../components/common/user/UserIcon";
+import IconButton from "../../../components/ui/IconButton";
+import Preloader from "../../../components/ui/Preloader";
 
 interface Props {
-    server: Servers.Server;
+	server: Servers.Server;
 }
 
 export function Invites({ server }: Props) {
-    const [invites, setInvites] = useState<InvitesNS.ServerInvite[] | undefined>(undefined);
+	const [invites, setInvites] = useState<
+		InvitesNS.ServerInvite[] | undefined
+	>(undefined);
+
+	const ctx = useForceUpdate();
+	const [deleting, setDelete] = useState<string[]>([]);
+	const users = useUsers(invites?.map((x) => x.creator) ?? [], ctx);
+	const channels = useChannels(invites?.map((x) => x.channel) ?? [], ctx);
 
-    const ctx = useForceUpdate();
-    const [deleting, setDelete] = useState<string[]>([]);
-    const users = useUsers(invites?.map(x => x.creator) ?? [], ctx);
-    const channels = useChannels(invites?.map(x => x.channel) ?? [], ctx);
+	useEffect(() => {
+		ctx.client.servers
+			.fetchInvites(server._id)
+			.then((invites) => setInvites(invites));
+	}, []);
 
-    useEffect(() => {
-        ctx.client.servers.fetchInvites(server._id)
-            .then(invites => setInvites(invites))
-    }, [ ]);
+	return (
+		<div className={styles.invites}>
+			<div className={styles.subtitle}>
+				<span>Invite Code</span>
+				<span>Invitor</span>
+				<span>Channel</span>
+				<span>Revoke</span>
+			</div>
+			{typeof invites === "undefined" && <Preloader type="ring" />}
+			{invites?.map((invite) => {
+				let creator = users.find((x) => x?._id === invite.creator);
+				let channel = channels.find((x) => x?._id === invite.channel);
 
-    return (
-        <div className={styles.invites}>
-            <div className={styles.subtitle}>
-                <span>Invite Code</span>
-                <span>Invitor</span>
-                <span>Channel</span>
-                <span>Revoke</span>
-            </div>
-            { typeof invites === 'undefined' && <Preloader type="ring" /> }
-            {
-                invites?.map(
-                    invite => {
-                        let creator = users.find(x => x?._id === invite.creator);
-                        let channel = channels.find(x => x?._id === invite.channel);
+				return (
+					<div
+						className={styles.invite}
+						data-deleting={deleting.indexOf(invite._id) > -1}>
+						<code>{invite._id}</code>
+						<span>
+							<UserIcon target={creator} size={24} />{" "}
+							{creator?.username ?? "unknown"}
+						</span>
+						<span>
+							{channel && creator
+								? getChannelName(ctx.client, channel, true)
+								: "#unknown"}
+						</span>
+						<IconButton
+							onClick={async () => {
+								setDelete([...deleting, invite._id]);
 
-                        return (
-                            <div className={styles.invite}
-                                data-deleting={deleting.indexOf(invite._id) > -1}>
-                                <code>{ invite._id }</code>
-                                <span>
-                                    <UserIcon target={creator} size={24} /> {creator?.username ?? 'unknown'}
-                                </span>
-                                <span>{ (channel && creator) ? getChannelName(ctx.client, channel, true) : '#unknown' }</span>
-                                <IconButton
-                                    onClick={async () => {
-                                        setDelete([
-                                            ...deleting,
-                                            invite._id
-                                        ]);
+								await ctx.client.deleteInvite(invite._id);
 
-                                        await ctx.client.deleteInvite(invite._id);
-                                        
-                                        setInvites(
-                                            invites?.filter(
-                                                x => x._id !== invite._id
-                                            )
-                                        );
-                                    }}
-                                    disabled={deleting.indexOf(invite._id) > -1}>
-                                    <XCircle size={24} />
-                                </IconButton>
-                            </div>
-                        )
-                    }
-                )
-            }
-        </div>
-    );
+								setInvites(
+									invites?.filter(
+										(x) => x._id !== invite._id,
+									),
+								);
+							}}
+							disabled={deleting.indexOf(invite._id) > -1}>
+							<XCircle size={24} />
+						</IconButton>
+					</div>
+				);
+			})}
+		</div>
+	);
 }
diff --git a/src/pages/settings/server/Members.tsx b/src/pages/settings/server/Members.tsx
index 8f0c171061e8caf37e54e7ff9ac7335fe824f10b..faff176047316e422e39aac488bb2def5f665313 100644
--- a/src/pages/settings/server/Members.tsx
+++ b/src/pages/settings/server/Members.tsx
@@ -1,34 +1,44 @@
-import styles from './Panes.module.scss';
-import { useEffect, useState } from "preact/hooks";
 import { Servers } from "revolt.js/dist/api/objects";
+
+import styles from "./Panes.module.scss";
+import { useEffect, useState } from "preact/hooks";
+
 import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
 
 interface Props {
-    server: Servers.Server;
+	server: Servers.Server;
 }
 
 // ! FIXME: bad code :)
 export function Members({ server }: Props) {
-    const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined);
+	const [members, setMembers] = useState<Servers.Member[] | undefined>(
+		undefined,
+	);
 
-    const ctx = useForceUpdate();
-    const users = useUsers(members?.map(x => x._id.user) ?? [], ctx);
+	const ctx = useForceUpdate();
+	const users = useUsers(members?.map((x) => x._id.user) ?? [], ctx);
 
-    useEffect(() => {
-        ctx.client.servers.members.fetchMembers(server._id)
-            .then(members => setMembers(members))
-    }, [ ]);
+	useEffect(() => {
+		ctx.client.servers.members
+			.fetchMembers(server._id)
+			.then((members) => setMembers(members));
+	}, []);
 
-    return (
-        <div className={styles.members}>
-            <div className={styles.subtitle}>
-                { members?.length ?? 0 } Members
-            </div>
-            { members && members.length > 0 && users?.map(x => x && 
-                <div className={styles.member}>
-                    <div>@{x.username}</div>
-                </div>) 
-            }
-        </div>
-    );
+	return (
+		<div className={styles.members}>
+			<div className={styles.subtitle}>
+				{members?.length ?? 0} Members
+			</div>
+			{members &&
+				members.length > 0 &&
+				users?.map(
+					(x) =>
+						x && (
+							<div className={styles.member}>
+								<div>@{x.username}</div>
+							</div>
+						),
+				)}
+		</div>
+	);
 }
diff --git a/src/pages/settings/server/Overview.tsx b/src/pages/settings/server/Overview.tsx
index 849e0e01517e595f002ae37042e4b787dfd35181..2f32ee73057a39a0ba1e66dec29f2db49c93b668 100644
--- a/src/pages/settings/server/Overview.tsx
+++ b/src/pages/settings/server/Overview.tsx
@@ -1,142 +1,193 @@
-import { Text } from "preact-i18n";
 import isEqual from "lodash.isequal";
-import styles from './Panes.module.scss';
-import Button from "../../../components/ui/Button";
-import InputBox from "../../../components/ui/InputBox";
-import ComboBox from "../../../components/ui/ComboBox";
 import { Servers, Server } from "revolt.js/dist/api/objects";
-import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
+
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
-import { getChannelName } from "../../../context/revoltjs/util";
-import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
+
 import { FileUploader } from "../../../context/revoltjs/FileUploads";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+import { getChannelName } from "../../../context/revoltjs/util";
+
+import Button from "../../../components/ui/Button";
+import ComboBox from "../../../components/ui/ComboBox";
+import InputBox from "../../../components/ui/InputBox";
 
 interface Props {
-    server: Servers.Server;
+	server: Servers.Server;
 }
 
 export function Overview({ server }: Props) {
-    const client = useContext(AppContext);
+	const client = useContext(AppContext);
+
+	const [name, setName] = useState(server.name);
+	const [description, setDescription] = useState(server.description ?? "");
+	const [systemMessages, setSystemMessages] = useState(
+		server.system_messages,
+	);
 
-    const [name, setName] = useState(server.name);
-    const [description, setDescription] = useState(server.description ?? '');
-    const [systemMessages, setSystemMessages] = useState(server.system_messages);
+	useEffect(() => setName(server.name), [server.name]);
+	useEffect(
+		() => setDescription(server.description ?? ""),
+		[server.description],
+	);
+	useEffect(
+		() => setSystemMessages(server.system_messages),
+		[server.system_messages],
+	);
 
-    useEffect(() => setName(server.name), [ server.name ]);
-    useEffect(() => setDescription(server.description ?? ''), [ server.description ]);
-    useEffect(() => setSystemMessages(server.system_messages), [ server.system_messages ]);
+	const [changed, setChanged] = useState(false);
+	function save() {
+		let changes: Partial<
+			Pick<Servers.Server, "name" | "description" | "system_messages">
+		> = {};
+		if (name !== server.name) changes.name = name;
+		if (description !== server.description)
+			changes.description = description;
+		if (!isEqual(systemMessages, server.system_messages))
+			changes.system_messages = systemMessages;
 
-    const [ changed, setChanged ] = useState(false);
-    function save() {
-        let changes: Partial<Pick<Servers.Server, 'name' | 'description' | 'system_messages'>> = {};
-        if (name !== server.name) changes.name = name;
-        if (description !== server.description) changes.description = description;
-        if (!isEqual(systemMessages, server.system_messages)) changes.system_messages = systemMessages;
-        
-        client.servers.edit(server._id, changes);
-        setChanged(false);
-    }
+		client.servers.edit(server._id, changes);
+		setChanged(false);
+	}
 
-    return (
-        <div className={styles.overview}>
-            <div className={styles.row}>
-                <FileUploader
-                    width={80}
-                    height={80}
-                    style="icon"
-                    fileType="icons"
-                    behaviour="upload"
-                    maxFileSize={2_500_000}
-                    onUpload={icon => client.servers.edit(server._id, { icon })}
-                    previewURL={client.servers.getIconURL(server._id, { max_side: 256 }, true)}
-                    remove={() => client.servers.edit(server._id, { remove: 'Icon' })}
-                />
-                <div className={styles.name}>
-                    <h3>
-                        <Text id="app.main.servers.name" />
-                    </h3>
-                    <InputBox
-                        contrast
-                        value={name}
-                        maxLength={32}
-                        onChange={e => {
-                            setName(e.currentTarget.value)
-                            if (!changed) setChanged(true)
-                        }}
-                    />
-                </div>
-            </div>
+	return (
+		<div className={styles.overview}>
+			<div className={styles.row}>
+				<FileUploader
+					width={80}
+					height={80}
+					style="icon"
+					fileType="icons"
+					behaviour="upload"
+					maxFileSize={2_500_000}
+					onUpload={(icon) =>
+						client.servers.edit(server._id, { icon })
+					}
+					previewURL={client.servers.getIconURL(
+						server._id,
+						{ max_side: 256 },
+						true,
+					)}
+					remove={() =>
+						client.servers.edit(server._id, { remove: "Icon" })
+					}
+				/>
+				<div className={styles.name}>
+					<h3>
+						<Text id="app.main.servers.name" />
+					</h3>
+					<InputBox
+						contrast
+						value={name}
+						maxLength={32}
+						onChange={(e) => {
+							setName(e.currentTarget.value);
+							if (!changed) setChanged(true);
+						}}
+					/>
+				</div>
+			</div>
 
-            <h3>
-                <Text id="app.main.servers.description" />
-            </h3>
-            <TextAreaAutoSize
-                maxRows={10}
-                minHeight={60}
-                maxLength={1024}
-                value={description}
-                placeholder={"Add a topic..."}
-                onChange={ev => {
-                    setDescription(ev.currentTarget.value);
-                    if (!changed) setChanged(true)
-                }}
-            />
+			<h3>
+				<Text id="app.main.servers.description" />
+			</h3>
+			<TextAreaAutoSize
+				maxRows={10}
+				minHeight={60}
+				maxLength={1024}
+				value={description}
+				placeholder={"Add a topic..."}
+				onChange={(ev) => {
+					setDescription(ev.currentTarget.value);
+					if (!changed) setChanged(true);
+				}}
+			/>
 
-            <h3>
-                <Text id="app.main.servers.custom_banner" />
-            </h3>
-            <FileUploader
-                height={160}
-                style="banner"
-                fileType="banners"
-                behaviour="upload"
-                maxFileSize={6_000_000}
-                onUpload={banner => client.servers.edit(server._id, { banner })}
-                previewURL={client.servers.getBannerURL(server._id, { width: 1000 }, true)}
-                remove={() => client.servers.edit(server._id, { remove: 'Banner' })}
-            />
+			<h3>
+				<Text id="app.main.servers.custom_banner" />
+			</h3>
+			<FileUploader
+				height={160}
+				style="banner"
+				fileType="banners"
+				behaviour="upload"
+				maxFileSize={6_000_000}
+				onUpload={(banner) =>
+					client.servers.edit(server._id, { banner })
+				}
+				previewURL={client.servers.getBannerURL(
+					server._id,
+					{ width: 1000 },
+					true,
+				)}
+				remove={() =>
+					client.servers.edit(server._id, { remove: "Banner" })
+				}
+			/>
 
-            <h3>
-                <Text id="app.settings.server_pages.overview.system_messages" />
-            </h3>
-            {[
-                [ 'User Joined', 'user_joined' ],
-                [ 'User Left', 'user_left' ],
-                [ 'User Kicked', 'user_kicked' ],
-                [ 'User Banned', 'user_banned' ]
-            ].map(([ i18n, key ]) =>
-                // ! FIXME: temporary code just so we can expose the options
-                <p style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
-                    <span style={{ flexShrink: '0', flex: `25%` }}>{i18n}</span>
-                    <ComboBox value={systemMessages?.[key as keyof typeof systemMessages] ?? 'disabled'}
-                        onChange={e => {
-                            if (!changed) setChanged(true)
-                            const v = e.currentTarget.value;
-                            if (v === 'disabled') {
-                                const { [key as keyof typeof systemMessages]: _, ...other } = systemMessages;
-                                setSystemMessages(other);
-                            } else {
-                                setSystemMessages({
-                                    ...systemMessages,
-                                    [key]: v
-                                });
-                            }
-                        }}>
-                        <option value='disabled'><Text id="general.disabled" /></option>
-                        { server.channels.map(id => {
-                            const channel = client.channels.get(id);
-                            if (!channel) return null;
-                            return <option value={id}>{ getChannelName(client, channel, true) }</option>;
-                        }) }
-                    </ComboBox>
-                </p>
-            )}
+			<h3>
+				<Text id="app.settings.server_pages.overview.system_messages" />
+			</h3>
+			{[
+				["User Joined", "user_joined"],
+				["User Left", "user_left"],
+				["User Kicked", "user_kicked"],
+				["User Banned", "user_banned"],
+			].map(([i18n, key]) => (
+				// ! FIXME: temporary code just so we can expose the options
+				<p
+					style={{
+						display: "flex",
+						gap: "8px",
+						alignItems: "center",
+					}}>
+					<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
+					<ComboBox
+						value={
+							systemMessages?.[
+								key as keyof typeof systemMessages
+							] ?? "disabled"
+						}
+						onChange={(e) => {
+							if (!changed) setChanged(true);
+							const v = e.currentTarget.value;
+							if (v === "disabled") {
+								const {
+									[key as keyof typeof systemMessages]: _,
+									...other
+								} = systemMessages;
+								setSystemMessages(other);
+							} else {
+								setSystemMessages({
+									...systemMessages,
+									[key]: v,
+								});
+							}
+						}}>
+						<option value="disabled">
+							<Text id="general.disabled" />
+						</option>
+						{server.channels.map((id) => {
+							const channel = client.channels.get(id);
+							if (!channel) return null;
+							return (
+								<option value={id}>
+									{getChannelName(client, channel, true)}
+								</option>
+							);
+						})}
+					</ComboBox>
+				</p>
+			))}
 
-            <p>
-                <Button onClick={save} contrast disabled={!changed}>
-                    <Text id="app.special.modals.actions.save" />
-                </Button>
-            </p>
-        </div>
-    );
+			<p>
+				<Button onClick={save} contrast disabled={!changed}>
+					<Text id="app.special.modals.actions.save" />
+				</Button>
+			</p>
+		</div>
+	);
 }
diff --git a/src/pages/settings/server/Roles.tsx b/src/pages/settings/server/Roles.tsx
index a139192903294e947e5959ae72147989b164e0a4..78f9b8113f35a90744cf45da21fc3418e02f6e65 100644
--- a/src/pages/settings/server/Roles.tsx
+++ b/src/pages/settings/server/Roles.tsx
@@ -1,120 +1,179 @@
-import { Text } from "preact-i18n";
-import styles from './Panes.module.scss';
-import Button from "../../../components/ui/Button";
-import Overline from "../../../components/ui/Overline";
+import { Plus } from "@styled-icons/boxicons-regular";
+import isEqual from "lodash.isequal";
 import { Servers } from "revolt.js/dist/api/objects";
-import Checkbox from "../../../components/ui/Checkbox";
+import {
+	ChannelPermission,
+	ServerPermission,
+} from "revolt.js/dist/api/permissions";
+
+import styles from "./Panes.module.scss";
+import { Text } from "preact-i18n";
 import { useContext, useEffect, useState } from "preact/hooks";
+
+import { useIntermediate } from "../../../context/intermediate/Intermediate";
 import { AppContext } from "../../../context/revoltjs/RevoltClient";
-import { ChannelPermission, ServerPermission } from "revolt.js/dist/api/permissions";
-import Tip from "../../../components/ui/Tip";
+
+import Button from "../../../components/ui/Button";
+import Checkbox from "../../../components/ui/Checkbox";
 import IconButton from "../../../components/ui/IconButton";
-import ButtonItem from "../../../components/navigation/items/ButtonItem";
-import isEqual from 'lodash.isequal';
 import InputBox from "../../../components/ui/InputBox";
-import { Plus } from "@styled-icons/boxicons-regular";
-import { useIntermediate } from "../../../context/intermediate/Intermediate";
+import Overline from "../../../components/ui/Overline";
+import Tip from "../../../components/ui/Tip";
+
+import ButtonItem from "../../../components/navigation/items/ButtonItem";
 
 interface Props {
-    server: Servers.Server;
+	server: Servers.Server;
 }
 
-const I32ToU32 = (arr: number[]) => arr.map(x => x >>> 0);
+const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
 
 // ! FIXME: bad code :)
 export function Roles({ server }: Props) {
-    const [ role, setRole ] = useState('default');
-    const { openScreen } = useIntermediate();
-    const client = useContext(AppContext);
-    const roles = server.roles ?? {};
+	const [role, setRole] = useState("default");
+	const { openScreen } = useIntermediate();
+	const client = useContext(AppContext);
+	const roles = server.roles ?? {};
+
+	if (role !== "default" && typeof roles[role] === "undefined") {
+		useEffect(() => setRole("default"));
+		return null;
+	}
 
-    if (role !== 'default' && typeof roles[role] === 'undefined') {
-        useEffect(() => setRole('default'));
-        return null;
-    }
+	const v = (id: string) =>
+		I32ToU32(
+			id === "default"
+				? server.default_permissions
+				: roles[id].permissions,
+		);
+	const [perm, setPerm] = useState(v(role));
+	useEffect(() => setPerm(v(role)), [role, roles[role]?.permissions]);
 
-    const v = (id: string) => I32ToU32(id === 'default' ? server.default_permissions : roles[id].permissions)
-    const [ perm, setPerm ] = useState(v(role));
-    useEffect(() => setPerm(v(role)), [ role, roles[role]?.permissions ]);
+	const modified = !isEqual(perm, v(role));
+	const save = () =>
+		client.servers.setPermissions(server._id, role, {
+			server: perm[0],
+			channel: perm[1],
+		});
+	const deleteRole = () => {
+		setRole("default");
+		client.servers.deleteRole(server._id, role);
+	};
 
-    const modified = !isEqual(perm, v(role));
-    const save = () => client.servers.setPermissions(server._id, role, { server: perm[0], channel: perm[1] });
-    const deleteRole = () => {
-        setRole('default');
-        client.servers.deleteRole(server._id, role);
-    };
-    
-    return (
-        <div className={styles.roles}>
-            <div className={styles.list}>
-                <div className={styles.title}>
-                    <h1><Text id="app.settings.server_pages.roles.title" /></h1>
-                    <Plus size={22} onClick={() =>
-                        openScreen({ id: 'special_input', type: 'create_role', server: server._id, callback: id => setRole(id) })} />
-                </div>
-                { [ 'default', ...Object.keys(roles) ]
-                    .map(id => {
-                        if (id === 'default') {
-                            return (
-                                <ButtonItem active={role === 'default'} onClick={() => setRole('default')}>
-                                    <Text id="app.settings.permissions.default_role" />
-                                </ButtonItem>
-                            )
-                        } else {
-                            return (
-                                <ButtonItem active={role === id} onClick={() => setRole(id)}>
-                                    { roles[id].name }
-                                </ButtonItem>
-                            )
-                        }
-                    })
-                }
-            </div>
-            <div className={styles.permissions}>
-                <div className={styles.title}>
-                    <h2>{ role === 'default' ? <Text id="app.settings.permissions.default_role" /> : roles[role].name }</h2>
-                    <Button contrast disabled={!modified} onClick={save}>Save</Button>
-                </div>
-                <section>
-                    <Overline type="subtle"><Text id="app.settings.permissions.server" /></Overline>
-                    { Object.keys(ServerPermission)
-                        .map(key => {
-                            if (key === 'View') return;
-                            let value = ServerPermission[key as keyof typeof ServerPermission];
+	return (
+		<div className={styles.roles}>
+			<div className={styles.list}>
+				<div className={styles.title}>
+					<h1>
+						<Text id="app.settings.server_pages.roles.title" />
+					</h1>
+					<Plus
+						size={22}
+						onClick={() =>
+							openScreen({
+								id: "special_input",
+								type: "create_role",
+								server: server._id,
+								callback: (id) => setRole(id),
+							})
+						}
+					/>
+				</div>
+				{["default", ...Object.keys(roles)].map((id) => {
+					if (id === "default") {
+						return (
+							<ButtonItem
+								active={role === "default"}
+								onClick={() => setRole("default")}>
+								<Text id="app.settings.permissions.default_role" />
+							</ButtonItem>
+						);
+					} else {
+						return (
+							<ButtonItem
+								active={role === id}
+								onClick={() => setRole(id)}>
+								{roles[id].name}
+							</ButtonItem>
+						);
+					}
+				})}
+			</div>
+			<div className={styles.permissions}>
+				<div className={styles.title}>
+					<h2>
+						{role === "default" ? (
+							<Text id="app.settings.permissions.default_role" />
+						) : (
+							roles[role].name
+						)}
+					</h2>
+					<Button contrast disabled={!modified} onClick={save}>
+						Save
+					</Button>
+				</div>
+				<section>
+					<Overline type="subtle">
+						<Text id="app.settings.permissions.server" />
+					</Overline>
+					{Object.keys(ServerPermission).map((key) => {
+						if (key === "View") return;
+						let value =
+							ServerPermission[
+								key as keyof typeof ServerPermission
+							];
 
-                            return (
-                                <Checkbox checked={(perm[0] & value) > 0}
-                                    onChange={() => setPerm([ perm[0] ^ value, perm[1] ])}
-                                    description={<Text id={`permissions.server.${key}.d`} />}>
-                                    <Text id={`permissions.server.${key}.t`} />
-                                </Checkbox>
-                            )
-                        })
-                    }
-                </section>
-                <section>
-                    <Overline type="subtle"><Text id="app.settings.permissions.channel" /></Overline>
-                    { Object.keys(ChannelPermission)
-                        .map(key => {
-                            if (key === 'ManageChannel') return;
-                            let value = ChannelPermission[key as keyof typeof ChannelPermission];
+						return (
+							<Checkbox
+								checked={(perm[0] & value) > 0}
+								onChange={() =>
+									setPerm([perm[0] ^ value, perm[1]])
+								}
+								description={
+									<Text id={`permissions.server.${key}.d`} />
+								}>
+								<Text id={`permissions.server.${key}.t`} />
+							</Checkbox>
+						);
+					})}
+				</section>
+				<section>
+					<Overline type="subtle">
+						<Text id="app.settings.permissions.channel" />
+					</Overline>
+					{Object.keys(ChannelPermission).map((key) => {
+						if (key === "ManageChannel") return;
+						let value =
+							ChannelPermission[
+								key as keyof typeof ChannelPermission
+							];
 
-                            return (
-                                <Checkbox checked={((perm[1] >>> 0) & value) > 0}
-                                    onChange={() => setPerm([ perm[0], perm[1] ^ value ])}
-                                    disabled={key === 'View'}
-                                    description={<Text id={`permissions.channel.${key}.d`} />}>
-                                    <Text id={`permissions.channel.${key}.t`} />
-                                </Checkbox>
-                            )
-                        })
-                    }
-                </section>
-                <div className={styles.actions}>
-                    <Button contrast disabled={!modified} onClick={save}>Save</Button>
-                    { role !== 'default' && <Button contrast error onClick={deleteRole}>Delete</Button> }
-                </div>
-            </div>
-        </div>
-    );
+						return (
+							<Checkbox
+								checked={((perm[1] >>> 0) & value) > 0}
+								onChange={() =>
+									setPerm([perm[0], perm[1] ^ value])
+								}
+								disabled={key === "View"}
+								description={
+									<Text id={`permissions.channel.${key}.d`} />
+								}>
+								<Text id={`permissions.channel.${key}.t`} />
+							</Checkbox>
+						);
+					})}
+				</section>
+				<div className={styles.actions}>
+					<Button contrast disabled={!modified} onClick={save}>
+						Save
+					</Button>
+					{role !== "default" && (
+						<Button contrast error onClick={deleteRole}>
+							Delete
+						</Button>
+					)}
+				</div>
+			</div>
+		</div>
+	);
 }
diff --git a/src/redux/State.tsx b/src/redux/State.tsx
index 2b3576ff463f5aef876a5963c455bd5f94d17215..7bc87e12d676ddeac2844a467b0a325df626c5ed 100644
--- a/src/redux/State.tsx
+++ b/src/redux/State.tsx
@@ -1,27 +1,28 @@
 import localForage from "localforage";
 import { Provider } from "react-redux";
-import { Children } from "../types/Preact";
-import { dispatch, State, store } from ".";
+
 import { useEffect, useState } from "preact/hooks";
 
+import { dispatch, State, store } from ".";
+import { Children } from "../types/Preact";
+
 interface Props {
-    children: Children;
+	children: Children;
 }
 
-export default function State(props: Props) {
-    const [loaded, setLoaded] = useState(false);
+export default function StateLoader(props: Props) {
+	const [loaded, setLoaded] = useState(false);
+
+	useEffect(() => {
+		localForage.getItem("state").then((state) => {
+			if (state !== null) {
+				dispatch({ type: "__INIT", state: state as State });
+			}
 
-    useEffect(() => {
-        localForage.getItem("state")
-            .then(state => {
-                if (state !== null) {
-                    dispatch({ type: "__INIT", state: state as State });
-                }
-                
-                setLoaded(true);
-            });
-    }, []);
+			setLoaded(true);
+		});
+	}, []);
 
-    if (!loaded) return null;
-    return <Provider store={store}>{props.children}</Provider>;
+	if (!loaded) return null;
+	return <Provider store={store}>{props.children}</Provider>;
 }
diff --git a/src/redux/connector.tsx b/src/redux/connector.tsx
index 6d9075cabcc0dd6891450442685edb1d5f3699c8..16eef2e7cdcfe75b47c110a63afd16c5fb837c5a 100644
--- a/src/redux/connector.tsx
+++ b/src/redux/connector.tsx
@@ -1,15 +1,16 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
+import { connect, ConnectedComponent } from "react-redux";
 
-import { State } from ".";
 import { h } from "preact";
 import { memo } from "preact/compat";
-import { connect, ConnectedComponent } from "react-redux";
+
+import { State } from ".";
 
 export function connectState<T>(
-    component: (props: any) => h.JSX.Element | null,
-    mapKeys: (state: State, props: T) => any,
-    memoize?: boolean
+	component: (props: any) => h.JSX.Element | null,
+	mapKeys: (state: State, props: T) => any,
+	memoize?: boolean,
 ): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
-    let c = connect(mapKeys)(component);
-    return memoize ? memo(c) : c;
+	let c = connect(mapKeys)(component);
+	return memoize ? memo(c) : c;
 }
diff --git a/src/redux/index.ts b/src/redux/index.ts
index e57d60ea1cc47d64e1b785bb60e060b73b20e389..b1969de87d30063fae1a490f5594ef3029d9fbaa 100644
--- a/src/redux/index.ts
+++ b/src/redux/index.ts
@@ -1,83 +1,84 @@
-import { createStore } from "redux";
 import localForage from "localforage";
-import rootReducer, { Action } from "./reducers";
-
+import { createStore } from "redux";
 import { Core } from "revolt.js/dist/api/objects";
-import { Typing } from "./reducers/typing";
-import { Drafts } from "./reducers/drafts";
-import { AuthState } from "./reducers/auth";
+
 import { Language } from "../context/Locale";
-import { Unreads } from "./reducers/unreads";
-import { SyncOptions } from "./reducers/sync";
-import { Settings } from "./reducers/settings";
-import { QueuedMessage } from "./reducers/queue";
+
+import rootReducer, { Action } from "./reducers";
+import { AuthState } from "./reducers/auth";
+import { Drafts } from "./reducers/drafts";
 import { ExperimentOptions } from "./reducers/experiments";
 import { LastOpened } from "./reducers/last_opened";
 import { Notifications } from "./reducers/notifications";
+import { QueuedMessage } from "./reducers/queue";
 import { SectionToggle } from "./reducers/section_toggle";
+import { Settings } from "./reducers/settings";
+import { SyncOptions } from "./reducers/sync";
+import { Typing } from "./reducers/typing";
+import { Unreads } from "./reducers/unreads";
 
 export type State = {
-    config: Core.RevoltNodeConfiguration,
-    locale: Language;
-    auth: AuthState;
-    settings: Settings;
-    unreads: Unreads;
-    queue: QueuedMessage[];
-    typing: Typing;
-    drafts: Drafts;
-    sync: SyncOptions;
-    experiments: ExperimentOptions;
-    lastOpened: LastOpened;
-    notifications: Notifications;
-    sectionToggle: SectionToggle;
+	config: Core.RevoltNodeConfiguration;
+	locale: Language;
+	auth: AuthState;
+	settings: Settings;
+	unreads: Unreads;
+	queue: QueuedMessage[];
+	typing: Typing;
+	drafts: Drafts;
+	sync: SyncOptions;
+	experiments: ExperimentOptions;
+	lastOpened: LastOpened;
+	notifications: Notifications;
+	sectionToggle: SectionToggle;
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const store = createStore((state: any, action: any) => {
-    if (import.meta.env.DEV) {
-        console.debug("State Update:", action);
-    }
+	if (import.meta.env.DEV) {
+		console.debug("State Update:", action);
+	}
 
-    if (action.type === "__INIT") {
-        return action.state;
-    }
+	if (action.type === "__INIT") {
+		return action.state;
+	}
 
-    return rootReducer(state, action);
+	return rootReducer(state, action);
 });
 
 // Save state using localForage.
 store.subscribe(() => {
-    const {
-        config,
-        locale,
-        auth,
-        settings,
-        unreads,
-        queue,
-        drafts,
-        sync,
-        experiments,
-        lastOpened,
-        notifications,
-        sectionToggle
-    } = store.getState() as State;
+	const {
+		config,
+		locale,
+		auth,
+		settings,
+		unreads,
+		queue,
+		drafts,
+		sync,
+		experiments,
+		lastOpened,
+		notifications,
+		sectionToggle,
+	} = store.getState() as State;
 
-    localForage.setItem("state", {
-        config,
-        locale,
-        auth,
-        settings,
-        unreads,
-        queue,
-        drafts,
-        sync,
-        experiments,
-        lastOpened,
-        notifications,
-        sectionToggle
-    });
+	localForage.setItem("state", {
+		config,
+		locale,
+		auth,
+		settings,
+		unreads,
+		queue,
+		drafts,
+		sync,
+		experiments,
+		lastOpened,
+		notifications,
+		sectionToggle,
+	});
 });
 
 export function dispatch(action: Action) {
-    store.dispatch(action);
+	store.dispatch(action);
 }
diff --git a/src/redux/reducers/auth.ts b/src/redux/reducers/auth.ts
index c309e4f833a7a1385a544bc51529a070462db0b2..a91a88ec2e893b5249919f57f4547edc83146110 100644
--- a/src/redux/reducers/auth.ts
+++ b/src/redux/reducers/auth.ts
@@ -1,49 +1,49 @@
 import type { Auth } from "revolt.js/dist/api/objects";
 
 export interface AuthState {
-    accounts: {
-        [key: string]: {
-            session: Auth.Session;
-        };
-    };
-    active?: string;
+	accounts: {
+		[key: string]: {
+			session: Auth.Session;
+		};
+	};
+	active?: string;
 }
 
 export type AuthAction =
-    | { type: undefined }
-    | {
-          type: "LOGIN";
-          session: Auth.Session;
-      }
-    | {
-          type: "LOGOUT";
-          user_id?: string;
-      };
+	| { type: undefined }
+	| {
+			type: "LOGIN";
+			session: Auth.Session;
+	  }
+	| {
+			type: "LOGOUT";
+			user_id?: string;
+	  };
 
 export function auth(
-    state = { accounts: {} } as AuthState,
-    action: AuthAction
+	state = { accounts: {} } as AuthState,
+	action: AuthAction,
 ): AuthState {
-    switch (action.type) {
-        case "LOGIN":
-            return {
-                accounts: {
-                    ...state.accounts,
-                    [action.session.user_id]: {
-                        session: action.session,
-                    },
-                },
-                active: action.session.user_id,
-            };
-        case "LOGOUT": {
-            const accounts = Object.assign({}, state.accounts);
-            action.user_id && delete accounts[action.user_id];
+	switch (action.type) {
+		case "LOGIN":
+			return {
+				accounts: {
+					...state.accounts,
+					[action.session.user_id]: {
+						session: action.session,
+					},
+				},
+				active: action.session.user_id,
+			};
+		case "LOGOUT": {
+			const accounts = Object.assign({}, state.accounts);
+			action.user_id && delete accounts[action.user_id];
 
-            return {
-                accounts,
-            };
-        }
-        default:
-            return state;
-    }
+			return {
+				accounts,
+			};
+		}
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/drafts.ts b/src/redux/reducers/drafts.ts
index 4f36a84655c7f2a5526674926f75e63133fcb7cc..d34c29bdcfade54ae17a390a3521031c753e7d0f 100644
--- a/src/redux/reducers/drafts.ts
+++ b/src/redux/reducers/drafts.ts
@@ -1,35 +1,35 @@
 export type Drafts = { [key: string]: string };
 
 export type DraftAction =
-    | { type: undefined }
-    | {
-          type: "SET_DRAFT";
-          channel: string;
-          content: string;
-      }
-    | {
-          type: "CLEAR_DRAFT";
-          channel: string;
-      }
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "SET_DRAFT";
+			channel: string;
+			content: string;
+	  }
+	| {
+			type: "CLEAR_DRAFT";
+			channel: string;
+	  }
+	| {
+			type: "RESET";
+	  };
 
 export function drafts(state: Drafts = {}, action: DraftAction): Drafts {
-    switch (action.type) {
-        case "SET_DRAFT":
-            return {
-                ...state,
-                [action.channel]: action.content,
-            };
-        case "CLEAR_DRAFT": {
-            // eslint-disable-next-line @typescript-eslint/no-unused-vars
-            const { [action.channel]: _, ...newState } = state;
-            return newState;
-        }
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "SET_DRAFT":
+			return {
+				...state,
+				[action.channel]: action.content,
+			};
+		case "CLEAR_DRAFT": {
+			// eslint-disable-next-line @typescript-eslint/no-unused-vars
+			const { [action.channel]: _, ...newState } = state;
+			return newState;
+		}
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/experiments.ts b/src/redux/reducers/experiments.ts
index fa3adaddd570be3f7d4a513d78cb85e951538ea8..1e3b90891f3ab7794756b2b5e6c5dca605f5d317 100644
--- a/src/redux/reducers/experiments.ts
+++ b/src/redux/reducers/experiments.ts
@@ -2,43 +2,43 @@ export type Experiments = never;
 export const AVAILABLE_EXPERIMENTS: Experiments[] = [];
 
 export interface ExperimentOptions {
-    enabled?: Experiments[];
+	enabled?: Experiments[];
 }
 
 export type ExperimentsAction =
-    | { type: undefined }
-    | {
-          type: "EXPERIMENTS_ENABLE";
-          key: Experiments;
-      }
-    | {
-          type: "EXPERIMENTS_DISABLE";
-          key: Experiments;
-      };
+	| { type: undefined }
+	| {
+			type: "EXPERIMENTS_ENABLE";
+			key: Experiments;
+	  }
+	| {
+			type: "EXPERIMENTS_DISABLE";
+			key: Experiments;
+	  };
 
 export function experiments(
-    state = {} as ExperimentOptions,
-    action: ExperimentsAction
+	state = {} as ExperimentOptions,
+	action: ExperimentsAction,
 ): ExperimentOptions {
-    switch (action.type) {
-        case "EXPERIMENTS_ENABLE":
-            return {
-                ...state,
-                enabled: [
-                    ...(state.enabled ?? [])
-                        .filter((x) => AVAILABLE_EXPERIMENTS.includes(x))
-                        .filter((v) => v !== action.key),
-                    action.key,
-                ],
-            };
-        case "EXPERIMENTS_DISABLE":
-            return {
-                ...state,
-                enabled: state.enabled
-                    ?.filter((v) => v !== action.key)
-                    .filter((x) => AVAILABLE_EXPERIMENTS.includes(x)),
-            };
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "EXPERIMENTS_ENABLE":
+			return {
+				...state,
+				enabled: [
+					...(state.enabled ?? [])
+						.filter((x) => AVAILABLE_EXPERIMENTS.includes(x))
+						.filter((v) => v !== action.key),
+					action.key,
+				],
+			};
+		case "EXPERIMENTS_DISABLE":
+			return {
+				...state,
+				enabled: state.enabled
+					?.filter((v) => v !== action.key)
+					.filter((x) => AVAILABLE_EXPERIMENTS.includes(x)),
+			};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts
index 15b7e36294bb0e4a0e21592e5353ad1e241edd9a..83e3fb3fdc5cf8444bdb83b89ba7ef395b6294df 100644
--- a/src/redux/reducers/index.ts
+++ b/src/redux/reducers/index.ts
@@ -1,60 +1,60 @@
-import { State } from "..";
 import { combineReducers } from "redux";
 
-import { config, ConfigAction } from "./server_config";
-import { settings, SettingsAction } from "./settings";
-import { locale, LocaleAction } from "./locale";
+import { State } from "..";
 import { auth, AuthAction } from "./auth";
-import { unreads, UnreadsAction } from "./unreads";
-import { queue, QueueAction } from "./queue";
-import { typing, TypingAction } from "./typing";
 import { drafts, DraftAction } from "./drafts";
-import { sync, SyncAction } from "./sync";
 import { experiments, ExperimentsAction } from "./experiments";
 import { lastOpened, LastOpenedAction } from "./last_opened";
+import { locale, LocaleAction } from "./locale";
 import { notifications, NotificationsAction } from "./notifications";
+import { queue, QueueAction } from "./queue";
 import { sectionToggle, SectionToggleAction } from "./section_toggle";
+import { config, ConfigAction } from "./server_config";
+import { settings, SettingsAction } from "./settings";
+import { sync, SyncAction } from "./sync";
+import { typing, TypingAction } from "./typing";
+import { unreads, UnreadsAction } from "./unreads";
 
 export default combineReducers({
-    config,
-    locale,
-    auth,
-    settings,
-    unreads,
-    queue,
-    typing,
-    drafts,
-    sync,
-    experiments,
-    lastOpened,
-    notifications,
-    sectionToggle
+	config,
+	locale,
+	auth,
+	settings,
+	unreads,
+	queue,
+	typing,
+	drafts,
+	sync,
+	experiments,
+	lastOpened,
+	notifications,
+	sectionToggle,
 });
 
 export type Action =
-    | ConfigAction
-    | LocaleAction
-    | AuthAction
-    | SettingsAction
-    | UnreadsAction
-    | QueueAction
-    | TypingAction
-    | DraftAction
-    | SyncAction
-    | ExperimentsAction
-    | LastOpenedAction
-    | NotificationsAction
-    | SectionToggleAction
-    | { type: "__INIT"; state: State };
+	| ConfigAction
+	| LocaleAction
+	| AuthAction
+	| SettingsAction
+	| UnreadsAction
+	| QueueAction
+	| TypingAction
+	| DraftAction
+	| SyncAction
+	| ExperimentsAction
+	| LastOpenedAction
+	| NotificationsAction
+	| SectionToggleAction
+	| { type: "__INIT"; state: State };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function filter(obj: any, keys: string[]) {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const newObj: any = {};
-    for (const key of keys) {
-        const v = obj[key];
-        if (v) newObj[key] = v;
-    }
+	// eslint-disable-next-line @typescript-eslint/no-explicit-any
+	const newObj: any = {};
+	for (const key of keys) {
+		const v = obj[key];
+		if (v) newObj[key] = v;
+	}
 
-    return newObj;
+	return newObj;
 }
diff --git a/src/redux/reducers/last_opened.ts b/src/redux/reducers/last_opened.ts
index 0b29a156acba4cefe70d7aae7f653464184db98d..b4c89d18837c27d8098c5df392f049f27925de88 100644
--- a/src/redux/reducers/last_opened.ts
+++ b/src/redux/reducers/last_opened.ts
@@ -1,29 +1,32 @@
 export interface LastOpened {
-    [key: string]: string
+	[key: string]: string;
 }
 
 export type LastOpenedAction =
-    | { type: undefined }
-    | {
-        type: "LAST_OPENED_SET";
-        parent: string;
-        child: string;
-      }
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "LAST_OPENED_SET";
+			parent: string;
+			child: string;
+	  }
+	| {
+			type: "RESET";
+	  };
 
-export function lastOpened(state = {} as LastOpened, action: LastOpenedAction): LastOpened {
-    switch (action.type) {
-        case "LAST_OPENED_SET": {
-            return {
-                ...state,
-                [action.parent]: action.child
-            }
-        }
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+export function lastOpened(
+	state = {} as LastOpened,
+	action: LastOpenedAction,
+): LastOpened {
+	switch (action.type) {
+		case "LAST_OPENED_SET": {
+			return {
+				...state,
+				[action.parent]: action.child,
+			};
+		}
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/locale.ts b/src/redux/reducers/locale.ts
index 6146dd53e6d3cb5e529f95b2fe17f2fda2d21a70..fd695a1e463ea08ee32d3e495a1ac109b6a0a875 100644
--- a/src/redux/reducers/locale.ts
+++ b/src/redux/reducers/locale.ts
@@ -1,51 +1,52 @@
 import { Language } from "../../context/Locale";
+
 import type { SyncUpdateAction } from "./sync";
 
 export type LocaleAction =
-    | { type: undefined }
-    | {
-          type: "SET_LOCALE";
-          locale: Language;
-      }
-    | SyncUpdateAction;
+	| { type: undefined }
+	| {
+			type: "SET_LOCALE";
+			locale: Language;
+	  }
+	| SyncUpdateAction;
 
 export function findLanguage(lang?: string): Language {
-    if (!lang) {
-        if (typeof navigator === "undefined") {
-            lang = Language.ENGLISH;
-        } else {
-            lang = navigator.language;
-        }
-    }
-
-    const code = lang.replace("-", "_");
-    const short = code.split("_")[0];
-
-    const values = [];
-    for (const key in Language) {
-        const value = Language[key as keyof typeof Language];
-        values.push(value);
-        if (value.startsWith(code)) {
-            return value as Language;
-        }
-    }
-
-    for (const value of values.reverse()) {
-        if (value.startsWith(short)) {
-            return value as Language;
-        }
-    }
-
-    return Language.ENGLISH;
+	if (!lang) {
+		if (typeof navigator === "undefined") {
+			lang = Language.ENGLISH;
+		} else {
+			lang = navigator.language;
+		}
+	}
+
+	const code = lang.replace("-", "_");
+	const short = code.split("_")[0];
+
+	const values = [];
+	for (const key in Language) {
+		const value = Language[key as keyof typeof Language];
+		values.push(value);
+		if (value.startsWith(code)) {
+			return value as Language;
+		}
+	}
+
+	for (const value of values.reverse()) {
+		if (value.startsWith(short)) {
+			return value as Language;
+		}
+	}
+
+	return Language.ENGLISH;
 }
 
 export function locale(state = findLanguage(), action: LocaleAction): Language {
-    switch (action.type) {
-        case "SET_LOCALE":
-            return action.locale;
-        case "SYNC_UPDATE":
-            return (action.update.locale?.[1] ?? state) as Language;
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "SET_LOCALE":
+			return action.locale;
+		case "SYNC_UPDATE":
+			return (action.update.locale?.[1] ?? state) as Language;
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/notifications.ts b/src/redux/reducers/notifications.ts
index 6ffd352e190291e933c872698e68e90ceba1cee3..cb0f34dcaa6b45439b94f75bc53c59ce475b9c5b 100644
--- a/src/redux/reducers/notifications.ts
+++ b/src/redux/reducers/notifications.ts
@@ -1,72 +1,82 @@
 import type { Channel, Message } from "revolt.js";
+
 import type { SyncUpdateAction } from "./sync";
 
-export type NotificationState = 'all' | 'mention' | 'none' | 'muted';
+export type NotificationState = "all" | "mention" | "none" | "muted";
 
 export type Notifications = {
-    [key: string]: NotificationState
-}
+	[key: string]: NotificationState;
+};
 
-export const DEFAULT_STATES: { [key in Channel['channel_type']]: NotificationState } = {
-    'SavedMessages': 'all',
-    'DirectMessage': 'all',
-    'Group': 'all',
-    'TextChannel': 'mention',
-    'VoiceChannel': 'mention'
+export const DEFAULT_STATES: {
+	[key in Channel["channel_type"]]: NotificationState;
+} = {
+	SavedMessages: "all",
+	DirectMessage: "all",
+	Group: "all",
+	TextChannel: "mention",
+	VoiceChannel: "mention",
 };
 
-export function getNotificationState(notifications: Notifications, channel: Channel) {
-    return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type];
+export function getNotificationState(
+	notifications: Notifications,
+	channel: Channel,
+) {
+	return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type];
 }
 
-export function shouldNotify(state: NotificationState, message: Message, user_id: string) {
-    switch (state) {
-        case 'muted':
-        case 'none': return false;
-        case 'mention': {
-            if (!message.mentions?.includes(user_id)) return false;
-        }
-    }
+export function shouldNotify(
+	state: NotificationState,
+	message: Message,
+	user_id: string,
+) {
+	switch (state) {
+		case "muted":
+		case "none":
+			return false;
+		case "mention": {
+			if (!message.mentions?.includes(user_id)) return false;
+		}
+	}
 
-    return true;
+	return true;
 }
 
 export type NotificationsAction =
-    | { type: undefined }
-    | {
-        type: "NOTIFICATIONS_SET";
-        key: string;
-        state: NotificationState;
-      }
-    | {
-        type: "NOTIFICATIONS_REMOVE";
-        key: string;
-      }
-    | SyncUpdateAction
-    | {
-        type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "NOTIFICATIONS_SET";
+			key: string;
+			state: NotificationState;
+	  }
+	| {
+			type: "NOTIFICATIONS_REMOVE";
+			key: string;
+	  }
+	| SyncUpdateAction
+	| {
+			type: "RESET";
+	  };
 
 export function notifications(
-    state = {} as Notifications,
-    action: NotificationsAction
+	state = {} as Notifications,
+	action: NotificationsAction,
 ): Notifications {
-    switch (action.type) {
-        case "NOTIFICATIONS_SET":
-            return {
-                ...state,
-                [action.key]: action.state
-            };
-        case "NOTIFICATIONS_REMOVE":
-            {
-                const { [action.key]: _, ...newState } = state;
-                return newState;
-            }
-        case "SYNC_UPDATE":
-            return action.update.notifications?.[1] ?? state;
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "NOTIFICATIONS_SET":
+			return {
+				...state,
+				[action.key]: action.state,
+			};
+		case "NOTIFICATIONS_REMOVE": {
+			const { [action.key]: _, ...newState } = state;
+			return newState;
+		}
+		case "SYNC_UPDATE":
+			return action.update.notifications?.[1] ?? state;
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/queue.ts b/src/redux/reducers/queue.ts
index ef64578edd0920622f538b94a7347bda19c212b4..20020e0ffdea55af43859f98307ac3c373220684 100644
--- a/src/redux/reducers/queue.ts
+++ b/src/redux/reducers/queue.ts
@@ -1,113 +1,113 @@
 import type { MessageObject } from "../../context/revoltjs/util";
 
 export enum QueueStatus {
-    SENDING = "sending",
-    ERRORED = "errored",
+	SENDING = "sending",
+	ERRORED = "errored",
 }
 
 export interface Reply {
-    id: string,
-    mention: boolean
+	id: string;
+	mention: boolean;
 }
 
-export type QueuedMessageData = Omit<MessageObject, 'content' | 'replies'> & {
-    content: string;
-    replies: Reply[];
-}
+export type QueuedMessageData = Omit<MessageObject, "content" | "replies"> & {
+	content: string;
+	replies: Reply[];
+};
 
 export interface QueuedMessage {
-    id: string;
-    channel: string;
-    data: QueuedMessageData;
-    status: QueueStatus;
-    error?: string;
+	id: string;
+	channel: string;
+	data: QueuedMessageData;
+	status: QueueStatus;
+	error?: string;
 }
 
 export type QueueAction =
-    | { type: undefined }
-    | {
-          type: "QUEUE_ADD";
-          nonce: string;
-          channel: string;
-          message: QueuedMessageData;
-      }
-    | {
-          type: "QUEUE_FAIL";
-          nonce: string;
-          error: string;
-      }
-    | {
-          type: "QUEUE_START";
-          nonce: string;
-      }
-    | {
-          type: "QUEUE_REMOVE";
-          nonce: string;
-      }
-    | {
-          type: "QUEUE_DROP_ALL";
-      }
-    | {
-          type: "QUEUE_FAIL_ALL";
-      }
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "QUEUE_ADD";
+			nonce: string;
+			channel: string;
+			message: QueuedMessageData;
+	  }
+	| {
+			type: "QUEUE_FAIL";
+			nonce: string;
+			error: string;
+	  }
+	| {
+			type: "QUEUE_START";
+			nonce: string;
+	  }
+	| {
+			type: "QUEUE_REMOVE";
+			nonce: string;
+	  }
+	| {
+			type: "QUEUE_DROP_ALL";
+	  }
+	| {
+			type: "QUEUE_FAIL_ALL";
+	  }
+	| {
+			type: "RESET";
+	  };
 
 export function queue(
-    state: QueuedMessage[] = [],
-    action: QueueAction
+	state: QueuedMessage[] = [],
+	action: QueueAction,
 ): QueuedMessage[] {
-    switch (action.type) {
-        case "QUEUE_ADD": {
-            return [
-                ...state.filter((x) => x.id !== action.nonce),
-                {
-                    id: action.nonce,
-                    data: action.message,
-                    channel: action.channel,
-                    status: QueueStatus.SENDING,
-                },
-            ];
-        }
-        case "QUEUE_FAIL": {
-            const entry = state.find(
-                (x) => x.id === action.nonce
-            ) as QueuedMessage;
-            return [
-                ...state.filter((x) => x.id !== action.nonce),
-                {
-                    ...entry,
-                    status: QueueStatus.ERRORED,
-                    error: action.error,
-                },
-            ];
-        }
-        case "QUEUE_START": {
-            const entry = state.find(
-                (x) => x.id === action.nonce
-            ) as QueuedMessage;
-            return [
-                ...state.filter((x) => x.id !== action.nonce),
-                {
-                    ...entry,
-                    status: QueueStatus.SENDING,
-                },
-            ];
-        }
-        case "QUEUE_REMOVE":
-            return state.filter((x) => x.id !== action.nonce);
-        case "QUEUE_FAIL_ALL":
-            return state.map((x) => {
-                return {
-                    ...x,
-                    status: QueueStatus.ERRORED,
-                };
-            });
-        case "QUEUE_DROP_ALL":
-        case "RESET":
-            return [];
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "QUEUE_ADD": {
+			return [
+				...state.filter((x) => x.id !== action.nonce),
+				{
+					id: action.nonce,
+					data: action.message,
+					channel: action.channel,
+					status: QueueStatus.SENDING,
+				},
+			];
+		}
+		case "QUEUE_FAIL": {
+			const entry = state.find(
+				(x) => x.id === action.nonce,
+			) as QueuedMessage;
+			return [
+				...state.filter((x) => x.id !== action.nonce),
+				{
+					...entry,
+					status: QueueStatus.ERRORED,
+					error: action.error,
+				},
+			];
+		}
+		case "QUEUE_START": {
+			const entry = state.find(
+				(x) => x.id === action.nonce,
+			) as QueuedMessage;
+			return [
+				...state.filter((x) => x.id !== action.nonce),
+				{
+					...entry,
+					status: QueueStatus.SENDING,
+				},
+			];
+		}
+		case "QUEUE_REMOVE":
+			return state.filter((x) => x.id !== action.nonce);
+		case "QUEUE_FAIL_ALL":
+			return state.map((x) => {
+				return {
+					...x,
+					status: QueueStatus.ERRORED,
+				};
+			});
+		case "QUEUE_DROP_ALL":
+		case "RESET":
+			return [];
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/section_toggle.ts b/src/redux/reducers/section_toggle.ts
index 26b23dcac096f90985f5383bb7c64c18de4dfc49..68657fb37be17b7099b198a1a42b3d9fcae88a8f 100644
--- a/src/redux/reducers/section_toggle.ts
+++ b/src/redux/reducers/section_toggle.ts
@@ -1,37 +1,40 @@
 export interface SectionToggle {
-    [key: string]: boolean
+	[key: string]: boolean;
 }
 
 export type SectionToggleAction =
-    | { type: undefined }
-    | {
-        type: "SECTION_TOGGLE_SET";
-        id: string;
-        state: boolean;
-      }
-    | {
-        type: "SECTION_TOGGLE_UNSET";
-        id: string;
-      }
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "SECTION_TOGGLE_SET";
+			id: string;
+			state: boolean;
+	  }
+	| {
+			type: "SECTION_TOGGLE_UNSET";
+			id: string;
+	  }
+	| {
+			type: "RESET";
+	  };
 
-export function sectionToggle(state = {} as SectionToggle, action: SectionToggleAction): SectionToggle {
-    switch (action.type) {
-        case "SECTION_TOGGLE_SET": {
-            return {
-                ...state,
-                [action.id]: action.state
-            }
-        }
-        case "SECTION_TOGGLE_UNSET": {
-            const { [action.id]: _, ...newState } = state;
-            return newState;
-        }
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+export function sectionToggle(
+	state = {} as SectionToggle,
+	action: SectionToggleAction,
+): SectionToggle {
+	switch (action.type) {
+		case "SECTION_TOGGLE_SET": {
+			return {
+				...state,
+				[action.id]: action.state,
+			};
+		}
+		case "SECTION_TOGGLE_UNSET": {
+			const { [action.id]: _, ...newState } = state;
+			return newState;
+		}
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/server_config.ts b/src/redux/reducers/server_config.ts
index 6046c5d100c8c638b829c31f1e49494c600c82d7..a33ef1632d250ce2300077b3fd3b8b15cd99b8e4 100644
--- a/src/redux/reducers/server_config.ts
+++ b/src/redux/reducers/server_config.ts
@@ -1,20 +1,20 @@
 import type { Core } from "revolt.js/dist/api/objects";
 
 export type ConfigAction =
-    | { type: undefined }
-    | {
-        type: "SET_CONFIG";
-        config: Core.RevoltNodeConfiguration;
-      };
+	| { type: undefined }
+	| {
+			type: "SET_CONFIG";
+			config: Core.RevoltNodeConfiguration;
+	  };
 
 export function config(
-    state = { } as Core.RevoltNodeConfiguration,
-    action: ConfigAction
+	state = {} as Core.RevoltNodeConfiguration,
+	action: ConfigAction,
 ): Core.RevoltNodeConfiguration {
-    switch (action.type) {
-        case "SET_CONFIG":
-            return action.config;
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "SET_CONFIG":
+			return action.config;
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/settings.ts b/src/redux/reducers/settings.ts
index 35f5f737edbae93063ad9a3ef145c4403cfe4120..cf7dd124efae32d672f81f0a376cb62dc6c8ce4e 100644
--- a/src/redux/reducers/settings.ts
+++ b/src/redux/reducers/settings.ts
@@ -1,110 +1,112 @@
-import { filter } from ".";
-import type { SyncUpdateAction } from "./sync";
-import type { Sounds } from "../../assets/sounds/Audio";
 import type { Theme, ThemeOptions } from "../../context/Theme";
+
 import { setEmojiPack } from "../../components/common/Emoji";
 
+import { filter } from ".";
+import type { Sounds } from "../../assets/sounds/Audio";
+import type { SyncUpdateAction } from "./sync";
+
 export type SoundOptions = {
-    [key in Sounds]?: boolean
-}
+	[key in Sounds]?: boolean;
+};
 
 export const DEFAULT_SOUNDS: SoundOptions = {
-    message: true,
-    outbound: false,
-    call_join: true,
-    call_leave: true
+	message: true,
+	outbound: false,
+	call_join: true,
+	call_leave: true,
 };
 
 export interface NotificationOptions {
-    desktopEnabled?: boolean;
-    sounds?: SoundOptions
+	desktopEnabled?: boolean;
+	sounds?: SoundOptions;
 }
 
 export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji";
 export interface AppearanceOptions {
-    emojiPack?: EmojiPacks;
+	emojiPack?: EmojiPacks;
 }
 
 export interface Settings {
-    theme?: ThemeOptions;
-    appearance?: AppearanceOptions;
-    notification?: NotificationOptions;
+	theme?: ThemeOptions;
+	appearance?: AppearanceOptions;
+	notification?: NotificationOptions;
 }
 
 export type SettingsAction =
-    | { type: undefined }
-    | {
-          type: "SETTINGS_SET_THEME";
-          theme: ThemeOptions;
-      }
-    | {
-          type: "SETTINGS_SET_THEME_OVERRIDE";
-          custom?: Partial<Theme>;
-      }
-    | {
-          type: "SETTINGS_SET_NOTIFICATION_OPTIONS";
-          options: NotificationOptions;
-      }
-    | {
-          type: "SETTINGS_SET_APPEARANCE";
-          options: Partial<AppearanceOptions>;
-      }
-    | SyncUpdateAction
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "SETTINGS_SET_THEME";
+			theme: ThemeOptions;
+	  }
+	| {
+			type: "SETTINGS_SET_THEME_OVERRIDE";
+			custom?: Partial<Theme>;
+	  }
+	| {
+			type: "SETTINGS_SET_NOTIFICATION_OPTIONS";
+			options: NotificationOptions;
+	  }
+	| {
+			type: "SETTINGS_SET_APPEARANCE";
+			options: Partial<AppearanceOptions>;
+	  }
+	| SyncUpdateAction
+	| {
+			type: "RESET";
+	  };
 
 export function settings(
-    state = {} as Settings,
-    action: SettingsAction
+	state = {} as Settings,
+	action: SettingsAction,
 ): Settings {
-    setEmojiPack(state.appearance?.emojiPack ?? 'mutant');
+	setEmojiPack(state.appearance?.emojiPack ?? "mutant");
 
-    switch (action.type) {
-        case "SETTINGS_SET_THEME":
-            return {
-                ...state,
-                theme: {
-                    ...filter(state.theme, ["custom", "preset", "ligatures"]),
-                    ...action.theme,
-                },
-            };
-        case "SETTINGS_SET_THEME_OVERRIDE":
-            return {
-                ...state,
-                theme: {
-                    ...state.theme,
-                    custom: {
-                        ...state.theme?.custom,
-                        ...action.custom,
-                    },
-                },
-            };
-        case "SETTINGS_SET_NOTIFICATION_OPTIONS":
-            return {
-                ...state,
-                notification: {
-                    ...state.notification,
-                    ...action.options,
-                },
-            };
-        case "SETTINGS_SET_APPEARANCE":
-            return {
-                ...state,
-                appearance: {
-                    ...filter(state.appearance, ["emojiPack"]),
-                    ...action.options,
-                },
-            };
-        case "SYNC_UPDATE":
-            return {
-                ...state,
-                appearance: action.update.appearance?.[1] ?? state.appearance,
-                theme: action.update.theme?.[1] ?? state.theme,
-            };
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "SETTINGS_SET_THEME":
+			return {
+				...state,
+				theme: {
+					...filter(state.theme, ["custom", "preset", "ligatures"]),
+					...action.theme,
+				},
+			};
+		case "SETTINGS_SET_THEME_OVERRIDE":
+			return {
+				...state,
+				theme: {
+					...state.theme,
+					custom: {
+						...state.theme?.custom,
+						...action.custom,
+					},
+				},
+			};
+		case "SETTINGS_SET_NOTIFICATION_OPTIONS":
+			return {
+				...state,
+				notification: {
+					...state.notification,
+					...action.options,
+				},
+			};
+		case "SETTINGS_SET_APPEARANCE":
+			return {
+				...state,
+				appearance: {
+					...filter(state.appearance, ["emojiPack"]),
+					...action.options,
+				},
+			};
+		case "SYNC_UPDATE":
+			return {
+				...state,
+				appearance: action.update.appearance?.[1] ?? state.appearance,
+				theme: action.update.theme?.[1] ?? state.theme,
+			};
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/sync.ts b/src/redux/reducers/sync.ts
index 099d98d6acccdf10843069820958ef2cc5bdd2e5..5e465ee48f8c32f566265475739d92a59878a390 100644
--- a/src/redux/reducers/sync.ts
+++ b/src/redux/reducers/sync.ts
@@ -1,93 +1,94 @@
-import type { AppearanceOptions } from "./settings";
 import type { Language } from "../../context/Locale";
 import type { ThemeOptions } from "../../context/Theme";
+
 import type { Notifications } from "./notifications";
+import type { AppearanceOptions } from "./settings";
 
 export type SyncKeys = "theme" | "appearance" | "locale" | "notifications";
 
 export interface SyncData {
-    locale?: Language;
-    theme?: ThemeOptions;
-    appearance?: AppearanceOptions;
-    notifications?: Notifications;
+	locale?: Language;
+	theme?: ThemeOptions;
+	appearance?: AppearanceOptions;
+	notifications?: Notifications;
 }
 
 export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [
-    "theme",
-    "appearance",
-    "locale",
-    "notifications"
+	"theme",
+	"appearance",
+	"locale",
+	"notifications",
 ];
 export interface SyncOptions {
-    disabled?: SyncKeys[];
-    revision?: {
-        [key: string]: number;
-    };
+	disabled?: SyncKeys[];
+	revision?: {
+		[key: string]: number;
+	};
 }
 
 export type SyncUpdateAction = {
-    type: "SYNC_UPDATE";
-    update: { [key in SyncKeys]?: [number, SyncData[key]] };
+	type: "SYNC_UPDATE";
+	update: { [key in SyncKeys]?: [number, SyncData[key]] };
 };
 
 export type SyncAction =
-    | { type: undefined }
-    | {
-          type: "SYNC_ENABLE_KEY";
-          key: SyncKeys;
-      }
-    | {
-          type: "SYNC_DISABLE_KEY";
-          key: SyncKeys;
-      }
-    | {
-          type: "SYNC_SET_REVISION";
-          key: SyncKeys;
-          timestamp: number;
-      }
-    | SyncUpdateAction;
+	| { type: undefined }
+	| {
+			type: "SYNC_ENABLE_KEY";
+			key: SyncKeys;
+	  }
+	| {
+			type: "SYNC_DISABLE_KEY";
+			key: SyncKeys;
+	  }
+	| {
+			type: "SYNC_SET_REVISION";
+			key: SyncKeys;
+			timestamp: number;
+	  }
+	| SyncUpdateAction;
 
 export function sync(
-    state = {} as SyncOptions,
-    action: SyncAction
+	state = {} as SyncOptions,
+	action: SyncAction,
 ): SyncOptions {
-    switch (action.type) {
-        case "SYNC_DISABLE_KEY":
-            return {
-                ...state,
-                disabled: [
-                    ...(state.disabled ?? []).filter((v) => v !== action.key),
-                    action.key,
-                ],
-            };
-        case "SYNC_ENABLE_KEY":
-            return {
-                ...state,
-                disabled: state.disabled?.filter((v) => v !== action.key),
-            };
-        case "SYNC_SET_REVISION":
-            return {
-                ...state,
-                revision: {
-                    ...state.revision,
-                    [action.key]: action.timestamp,
-                },
-            };
-        case "SYNC_UPDATE": {
-            const revision = { ...state.revision };
-            for (const key of Object.keys(action.update)) {
-                const value = action.update[key as SyncKeys];
-                if (value) {
-                    revision[key] = value[0];
-                }
-            }
+	switch (action.type) {
+		case "SYNC_DISABLE_KEY":
+			return {
+				...state,
+				disabled: [
+					...(state.disabled ?? []).filter((v) => v !== action.key),
+					action.key,
+				],
+			};
+		case "SYNC_ENABLE_KEY":
+			return {
+				...state,
+				disabled: state.disabled?.filter((v) => v !== action.key),
+			};
+		case "SYNC_SET_REVISION":
+			return {
+				...state,
+				revision: {
+					...state.revision,
+					[action.key]: action.timestamp,
+				},
+			};
+		case "SYNC_UPDATE": {
+			const revision = { ...state.revision };
+			for (const key of Object.keys(action.update)) {
+				const value = action.update[key as SyncKeys];
+				if (value) {
+					revision[key] = value[0];
+				}
+			}
 
-            return {
-                ...state,
-                revision,
-            };
-        }
-        default:
-            return state;
-    }
+			return {
+				...state,
+				revision,
+			};
+		}
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/typing.ts b/src/redux/reducers/typing.ts
index 7ec152aa18cb3663e17f9e96a3594d6b346fc699..9dcbc594cbf4f2ed0c8b28434416ad7c0b3890df 100644
--- a/src/redux/reducers/typing.ts
+++ b/src/redux/reducers/typing.ts
@@ -2,47 +2,47 @@ export type TypingUser = { id: string; started: number };
 export type Typing = { [key: string]: TypingUser[] };
 
 export type TypingAction =
-    | { type: undefined }
-    | {
-          type: "TYPING_START";
-          channel: string;
-          user: string;
-      }
-    | {
-          type: "TYPING_STOP";
-          channel: string;
-          user: string;
-      }
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "TYPING_START";
+			channel: string;
+			user: string;
+	  }
+	| {
+			type: "TYPING_STOP";
+			channel: string;
+			user: string;
+	  }
+	| {
+			type: "RESET";
+	  };
 
 export function typing(state: Typing = {}, action: TypingAction): Typing {
-    switch (action.type) {
-        case "TYPING_START":
-            return {
-                ...state,
-                [action.channel]: [
-                    ...(state[action.channel] ?? []).filter(
-                        (x) => x.id !== action.user
-                    ),
-                    {
-                        id: action.user,
-                        started: +new Date(),
-                    },
-                ],
-            };
-        case "TYPING_STOP":
-            return {
-                ...state,
-                [action.channel]:
-                    state[action.channel]?.filter(
-                        (x) => x.id !== action.user
-                    ) ?? [],
-            };
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+	switch (action.type) {
+		case "TYPING_START":
+			return {
+				...state,
+				[action.channel]: [
+					...(state[action.channel] ?? []).filter(
+						(x) => x.id !== action.user,
+					),
+					{
+						id: action.user,
+						started: +new Date(),
+					},
+				],
+			};
+		case "TYPING_STOP":
+			return {
+				...state,
+				[action.channel]:
+					state[action.channel]?.filter(
+						(x) => x.id !== action.user,
+					) ?? [],
+			};
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/redux/reducers/unreads.ts b/src/redux/reducers/unreads.ts
index 1f1b2fb15e16da8610873dfccd14a2bef30db819..e9e1a9a7a40bbc1f9b7c73cbc38751de533d5b01 100644
--- a/src/redux/reducers/unreads.ts
+++ b/src/redux/reducers/unreads.ts
@@ -1,61 +1,61 @@
 import type { Sync } from "revolt.js/dist/api/objects";
 
 export interface Unreads {
-    [key: string]: Partial<Omit<Sync.ChannelUnread, "_id">>;
+	[key: string]: Partial<Omit<Sync.ChannelUnread, "_id">>;
 }
 
 export type UnreadsAction =
-    | { type: undefined }
-    | {
-          type: "UNREADS_MARK_READ";
-          channel: string;
-          message: string;
-      }
-    | {
-          type: "UNREADS_SET";
-          unreads: Sync.ChannelUnread[];
-      }
-    | {
-          type: "UNREADS_MENTION";
-          channel: string;
-          message: string;
-      }
-    | {
-          type: "RESET";
-      };
+	| { type: undefined }
+	| {
+			type: "UNREADS_MARK_READ";
+			channel: string;
+			message: string;
+	  }
+	| {
+			type: "UNREADS_SET";
+			unreads: Sync.ChannelUnread[];
+	  }
+	| {
+			type: "UNREADS_MENTION";
+			channel: string;
+			message: string;
+	  }
+	| {
+			type: "RESET";
+	  };
 
 export function unreads(state = {} as Unreads, action: UnreadsAction): Unreads {
-    switch (action.type) {
-        case "UNREADS_MARK_READ":
-            return {
-                ...state,
-                [action.channel]: {
-                    last_id: action.message,
-                },
-            };
-        case "UNREADS_SET": {
-            const obj: Unreads = {};
-            for (const entry of action.unreads) {
-                const { _id, ...v } = entry;
-                obj[_id.channel] = v;
-            }
+	switch (action.type) {
+		case "UNREADS_MARK_READ":
+			return {
+				...state,
+				[action.channel]: {
+					last_id: action.message,
+				},
+			};
+		case "UNREADS_SET": {
+			const obj: Unreads = {};
+			for (const entry of action.unreads) {
+				const { _id, ...v } = entry;
+				obj[_id.channel] = v;
+			}
 
-            return obj;
-        }
-        case "UNREADS_MENTION": {
-            const obj = state[action.channel];
+			return obj;
+		}
+		case "UNREADS_MENTION": {
+			const obj = state[action.channel];
 
-            return {
-                ...state,
-                [action.channel]: {
-                    ...obj,
-                    mentions: [...(obj?.mentions ?? []), action.message],
-                },
-            };
-        }
-        case "RESET":
-            return {};
-        default:
-            return state;
-    }
+			return {
+				...state,
+				[action.channel]: {
+					...obj,
+					mentions: [...(obj?.mentions ?? []), action.message],
+				},
+			};
+		}
+		case "RESET":
+			return {};
+		default:
+			return state;
+	}
 }
diff --git a/src/revision.ts b/src/revision.ts
index e01dcdda3515714a15fa6c6b1226800b29ad3f5b..6cb9d248887b1c1a16d18af1f2d631110ad3c910 100644
--- a/src/revision.ts
+++ b/src/revision.ts
@@ -1,3 +1,3 @@
-export const REPO_URL = 'https://gitlab.insrt.uk/revolt/revite/-/commit';
-export const GIT_REVISION = '__GIT_REVISION__';
-export const GIT_BRANCH: string = '__GIT_BRANCH__';
+export const REPO_URL = "https://gitlab.insrt.uk/revolt/revite/-/commit";
+export const GIT_REVISION = "__GIT_REVISION__";
+export const GIT_BRANCH: string = "__GIT_BRANCH__";
diff --git a/src/sw.ts b/src/sw.ts
index 7d58aba381355dbda79f57bb166e0e0466dd05d0..bf237fa2230384c354cc858bebb94bf7f2641f3b 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -1,48 +1,51 @@
 /// <reference lib="webworker" />
-import { precacheAndRoute } from 'workbox-precaching'
-import { Server } from 'revolt.js/dist/api/objects'
-import { Channel, Message, User } from 'revolt.js'
-import { IDBPDatabase, openDB } from 'idb'
-import { getItem } from 'localforage'
-import type { State } from './redux'
-import { getNotificationState, shouldNotify } from './redux/reducers/notifications'
+import { IDBPDatabase, openDB } from "idb";
+import { getItem } from "localforage";
+import { Channel, Message, User } from "revolt.js";
+import { Server } from "revolt.js/dist/api/objects";
+import { precacheAndRoute } from "workbox-precaching";
 
-declare let self: ServiceWorkerGlobalScope
+import type { State } from "./redux";
+import {
+	getNotificationState,
+	shouldNotify,
+} from "./redux/reducers/notifications";
 
-self.addEventListener('message', (event) => {
-	if (event.data && event.data.type === 'SKIP_WAITING')
-	self.skipWaiting()
-})
+declare let self: ServiceWorkerGlobalScope;
 
-precacheAndRoute(self.__WB_MANIFEST)
+self.addEventListener("message", (event) => {
+	if (event.data && event.data.type === "SKIP_WAITING") self.skipWaiting();
+});
+
+precacheAndRoute(self.__WB_MANIFEST);
 
 // ulid decodeTime(id: string)
 // since crypto is not available in sw.js
-const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
-const ENCODING_LEN = ENCODING.length
-const TIME_LEN = 10
+const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+const ENCODING_LEN = ENCODING.length;
+const TIME_LEN = 10;
 
 function decodeTime(id: string) {
-    var time = id.substr(0, TIME_LEN)
+	var time = id
+		.substr(0, TIME_LEN)
 		.split("")
 		.reverse()
 		.reduce(function (carry, char, index) {
 			var encodingIndex = ENCODING.indexOf(char);
-			if (encodingIndex === -1)
-				throw "invalid character found: " + char;
-			
-			return carry += encodingIndex * Math.pow(ENCODING_LEN, index);
+			if (encodingIndex === -1) throw "invalid character found: " + char;
+
+			return (carry += encodingIndex * Math.pow(ENCODING_LEN, index));
 		}, 0);
-	
-    return time;
+
+	return time;
 }
 
-self.addEventListener("push", event => {
+self.addEventListener("push", (event) => {
 	async function process() {
 		if (event.data === null) return;
 		let data: Message = event.data.json();
 
-		let item = await localStorage.getItem('state');
+		let item = await localStorage.getItem("state");
 		if (!item) return;
 
 		const state: State = JSON.parse(item);
@@ -52,36 +55,46 @@ self.addEventListener("push", event => {
 		let db: IDBPDatabase;
 		try {
 			// Match RevoltClient.tsx#L55
-			db = await openDB('state', 3, {
+			db = await openDB("state", 3, {
 				upgrade(db) {
-					for (let store of [ "channels", "servers", "users", "members" ]) {
+					for (let store of [
+						"channels",
+						"servers",
+						"users",
+						"members",
+					]) {
 						db.createObjectStore(store, {
-							keyPath: '_id'
+							keyPath: "_id",
 						});
 					}
 				},
 			});
 		} catch (err) {
-			console.error('Failed to open IndexedDB store, continuing without.');
+			console.error(
+				"Failed to open IndexedDB store, continuing without.",
+			);
 			return;
 		}
 
-		async function get<T>(store: string, key: string): Promise<T | undefined> {
+		async function get<T>(
+			store: string,
+			key: string,
+		): Promise<T | undefined> {
 			try {
 				return await db.get(store, key);
 			} catch (err) {
 				return undefined;
 			}
 		}
-		
-		let channel = await get<Channel>('channels', data.channel);
-		let user = await get<User>('users', data.author);
+
+		let channel = await get<Channel>("channels", data.channel);
+		let user = await get<User>("users", data.author);
 
 		if (channel) {
 			const notifs = getNotificationState(state.notifications, channel);
 			if (!shouldNotify(notifs, data, user_id)) return;
 		}
-		
+
 		let title = `@${data.author}`;
 		let username = user?.username ?? data.author;
 		let image;
@@ -91,12 +104,15 @@ self.addEventListener("push", event => {
 				image = `${autumn_url}/${attachment.tag}/${attachment._id}`;
 			}
 		}
-		
+
 		switch (channel?.channel_type) {
-			case "SavedMessages": break;
-			case "DirectMessage": title = `@${username}`; break;
+			case "SavedMessages":
+				break;
+			case "DirectMessage":
+				title = `@${username}`;
+				break;
 			case "Group":
-				if (user?._id === '00000000000000000000000000') {
+				if (user?._id === "00000000000000000000000000") {
 					title = channel.name;
 				} else {
 					title = `@${user?.username} - ${channel.name}`;
@@ -104,35 +120,43 @@ self.addEventListener("push", event => {
 				break;
 			case "TextChannel":
 				{
-					let server = await get<Server>('servers', channel.server);
+					let server = await get<Server>("servers", channel.server);
 					title = `@${user?.username} (#${channel.name}, ${server?.name})`;
 				}
 				break;
 		}
-		
+
 		await self.registration.showNotification(title, {
-			icon: user?.avatar ? `${autumn_url}/${user.avatar.tag}/${user.avatar._id}` : `https://api.revolt.chat/users/${data.author}/default_avatar`,
+			icon: user?.avatar
+				? `${autumn_url}/${user.avatar.tag}/${user.avatar._id}`
+				: `https://api.revolt.chat/users/${data.author}/default_avatar`,
 			image,
-			body: typeof data.content === "string" ? data.content : JSON.stringify(data.content),
+			body:
+				typeof data.content === "string"
+					? data.content
+					: JSON.stringify(data.content),
 			timestamp: decodeTime(data._id),
 			tag: data.channel,
 			badge: "https://app.revolt.chat/assets/icons/android-chrome-512x512.png",
-			data: channel?.channel_type === 'TextChannel' ? `/server/${channel.server}/channel/${channel._id}` : `/channel/${data.channel}`
+			data:
+				channel?.channel_type === "TextChannel"
+					? `/server/${channel.server}/channel/${channel._id}`
+					: `/channel/${data.channel}`,
 		});
 	}
-		
+
 	event.waitUntil(process());
 });
 
 // ? Open the app on notification click.
 // https://stackoverflow.com/a/39457287
-self.addEventListener("notificationclick", function(event) {
+self.addEventListener("notificationclick", function (event) {
 	let url = event.notification.data;
 	event.notification.close();
 	event.waitUntil(
 		self.clients
 			.matchAll({ includeUncontrolled: true, type: "window" })
-			.then(windowClients => {
+			.then((windowClients) => {
 				// Check if there is already a window/tab open with the target URL
 				for (var i = 0; i < windowClients.length; i++) {
 					var client = windowClients[i];
@@ -146,7 +170,6 @@ self.addEventListener("notificationclick", function(event) {
 				if (self.clients.openWindow) {
 					return self.clients.openWindow(url);
 				}
-			})
+			}),
 	);
 });
-	
\ No newline at end of file