state.instance.pleromaChatMessagesAvailable
+ }),
+ ...mapGetters(['unreadChatCount'])
},
methods: {
toggleDrawer () {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 5541b2cf..4a4e7610 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -47,6 +47,23 @@
{{ $t("nav.timelines") }}
+
+
+ {{ $t("nav.chats") }}
+
+ {{ unreadChatCount }}
+
+
+
-
@@ -69,11 +86,27 @@
-
{{ $t("nav.chat") }}
+=======
+ v-if="currentUser || !privateMode"
+ @click="toggleDrawer"
+ >
+
+ {{ $t("nav.public_tl") }}
+
+
+ -
+
+ {{ $t("nav.twkn") }}
+>>>>>>> develop
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 67d9bd3c..df095de3 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -14,11 +14,12 @@ const StatusContent = {
'status',
'focused',
'noHeading',
- 'fullContent'
+ 'fullContent',
+ 'singleLine'
],
data () {
return {
- showingTall: this.inConversation && this.focused,
+ showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 8068d8d2..bf8d376e 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -43,6 +43,7 @@
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js
index dcb56106..818e8bd5 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.js
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.js
@@ -7,7 +7,7 @@ function showWhoToFollow (panel, reply) {
panel.usersToFollow.forEach((toFollow, index) => {
let user = shuffled[index]
- let img = user.avatar || '/images/avi.png'
+ let img = user.avatar || this.$store.state.instance.defaultAvatar
let name = user.acct
toFollow.img = img
@@ -38,13 +38,7 @@ function getWhoToFollow (panel) {
const WhoToFollowPanel = {
data: () => ({
- usersToFollow: new Array(3).fill().map(x => (
- {
- img: '/images/avi.png',
- name: '',
- id: 0
- }
- ))
+ usersToFollow: []
}),
computed: {
user: function () {
@@ -68,6 +62,13 @@ const WhoToFollowPanel = {
},
mounted:
function () {
+ this.usersToFollow = new Array(3).fill().map(x => (
+ {
+ img: this.$store.state.instance.defaultAvatar,
+ name: '',
+ id: 0
+ }
+ ))
if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss
index 4cefe2be..1a26eb8d 100644
--- a/src/hocs/with_load_more/with_load_more.scss
+++ b/src/hocs/with_load_more/with_load_more.scss
@@ -12,5 +12,9 @@
.error {
font-size: 14px;
}
+
+ a {
+ cursor: pointer;
+ }
}
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 254701b4..b62ca3b3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -44,6 +44,7 @@
},
"features_panel": {
"chat": "Chat",
+ "pleroma_chat_messages": "Pleroma Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
"scope_options": "Scope options",
@@ -125,7 +126,8 @@
"search": "Search",
"who_to_follow": "Who to follow",
"preferences": "Preferences",
- "timelines": "Timelines"
+ "timelines": "Timelines",
+ "chats": "Chats"
},
"notifications": {
"broken_favorite": "Unknown status, searching for it…",
@@ -288,12 +290,11 @@
"change_password": "Change Password",
"change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!",
+ "chatMessageRadius": "Chat message",
"collapse_subject": "Collapse posts with subjects",
"composing": "Composing",
"confirm_new_password": "Confirm new password",
- "current_avatar": "Your current avatar",
"current_password": "Current password",
- "current_profile_banner": "Your current profile banner",
"mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data Import / Export",
"default_vis": "Default visibility scope",
@@ -400,6 +401,12 @@
"set_new_avatar": "Set new avatar",
"set_new_profile_background": "Set new profile background",
"set_new_profile_banner": "Set new profile banner",
+ "reset_avatar": "Reset avatar",
+ "reset_profile_background": "Reset profile background",
+ "reset_profile_banner": "Reset profile banner",
+ "reset_avatar_confirm": "Do you really want to reset the avatar?",
+ "reset_banner_confirm": "Do you really want to reset the banner?",
+ "reset_background_confirm": "Do you really want to reset the background?",
"settings": "Settings",
"subject_input_always_show": "Always show subject field",
"subject_line_behavior": "Copy subject when replying",
@@ -515,7 +522,12 @@
"selectedMenu": "Selected menu item",
"disabled": "Disabled",
"toggled": "Toggled",
- "tabs": "Tabs"
+ "tabs": "Tabs",
+ "chat": {
+ "incoming": "Incoming",
+ "outgoing": "Outgoing",
+ "border": "Border"
+ }
},
"radii": {
"_tab_label": "Roundness"
@@ -674,6 +686,7 @@
"its_you": "It's you!",
"media": "Media",
"mention": "Mention",
+ "message": "Message",
"mute": "Mute",
"muted": "Muted",
"per_day": "per day",
@@ -772,5 +785,27 @@
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
"password_reset_required": "You must reset your password to log in.",
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
+ },
+ "chats": {
+ "you": "You:",
+ "message_user": "Message {nickname}",
+ "delete": "Delete",
+ "chats": "Chats",
+ "new": "New Chat",
+ "empty_message_error": "Cannot post empty message",
+ "more": "More",
+ "delete_confirm": "Do you really want to delete this message?",
+ "error_loading_chat": "Something went wrong when loading the chat.",
+ "error_sending_message": "Something went wrong when sending the message.",
+ "empty_chat_list_placeholder": "You don't have any chats yet. Start a new chat!"
+ },
+ "file_type": {
+ "audio": "Audio",
+ "video": "Video",
+ "image": "Image",
+ "file": "File"
+ },
+ "display_date": {
+ "today": "Today"
}
}
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index f3c2e528..e7667d54 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -28,7 +28,12 @@
"disable": "Poista käytöstä",
"confirm": "Hyväksy",
"verify": "Varmenna",
- "enable": "Ota käyttöön"
+ "enable": "Ota käyttöön",
+ "loading": "Ladataan…",
+ "error_retry": "Yritä uudelleen",
+ "retry": "Yritä uudelleen",
+ "close": "Sulje",
+ "peek": "Kurkkaa"
},
"login": {
"login": "Kirjaudu sisään",
@@ -63,7 +68,8 @@
"who_to_follow": "Seurausehdotukset",
"preferences": "Asetukset",
"administration": "Ylläpito",
- "search": "Haku"
+ "search": "Haku",
+ "bookmarks": "Kirjanmerkit"
},
"notifications": {
"broken_favorite": "Viestiä ei löydetty…",
@@ -126,7 +132,12 @@
"public": "Tämä viesti näkyy kaikille",
"private": "Tämä viesti näkyy vain sinun seuraajillesi",
"unlisted": "Tämä viesti ei näy Julkisella Aikajanalla tai Koko Tunnettu Verkosto -aikajanalla"
- }
+ },
+ "preview": "Esikatselu",
+ "preview_empty": "Tyhjä",
+ "empty_status_error": "Tyhjää viestiä ilman tiedostoja ei voi lähettää",
+ "media_description": "Tiedoston kuvaus",
+ "media_description_error": "Tiedostojen päivitys epäonnistui, yritä uudelleen"
},
"registration": {
"bio": "Kuvaus",
@@ -175,7 +186,7 @@
"data_import_export_tab": "Tietojen tuonti / vienti",
"default_vis": "Oletusnäkyvyysrajaus",
"delete_account": "Poista tili",
- "delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
+ "delete_account_description": "Poista tietosi ja lukitse tili pysyvästi.",
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
"emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla",
@@ -329,7 +340,7 @@
"post_status_content_type": "Uuden viestin sisällön muoto",
"user_mutes": "Käyttäjät",
"useStreamingApiWarning": "(Kokeellinen)",
- "type_domains_to_mute": "Syötä mykistettäviä sivustoja",
+ "type_domains_to_mute": "Etsi mykistettäviä sivustoja",
"upload_a_photo": "Lataa kuva",
"fun": "Hupi",
"greentext": "Meeminuolet",
@@ -490,7 +501,21 @@
"title": "Versio",
"backend_version": "Palvelimen versio",
"frontend_version": "Käyttöliittymän versio"
- }
+ },
+ "reset_profile_background": "Nollaa taustakuva",
+ "reset_background_confirm": "Haluatko todella nollata taustakuvan?",
+ "mutes_and_blocks": "Mykistykset ja Estot",
+ "bot": "Tämä on bottitili",
+ "profile_fields": {
+ "label": "Profiilin metatiedot",
+ "add_field": "Lisää kenttä",
+ "name": "Nimi",
+ "value": "Sisältö"
+ },
+ "reset_avatar": "Nollaa profiilikuva",
+ "reset_profile_banner": "Nollaa profiilin tausta",
+ "reset_avatar_confirm": "Haluatko todella nollata profiilikuvan?",
+ "reset_banner_confirm": "Haluatko todella nollata profiilin taustan?"
},
"time": {
"day": "{0} päivä",
@@ -536,7 +561,8 @@
"show_new": "Näytä uudet",
"up_to_date": "Ajantasalla",
"no_more_statuses": "Ei enempää viestejä",
- "no_statuses": "Ei viestejä"
+ "no_statuses": "Ei viestejä",
+ "reload": "Päivitä"
},
"status": {
"favorites": "Tykkäykset",
@@ -551,7 +577,15 @@
"mute_conversation": "Mykistä keskustelu",
"unmute_conversation": "Poista mykistys",
"status_unavailable": "Viesti ei saatavissa",
- "copy_link": "Kopioi linkki"
+ "copy_link": "Kopioi linkki",
+ "bookmark": "Lisää kirjanmerkkeihin",
+ "unbookmark": "Poista kirjanmerkeistä",
+ "thread_muted": "Keskustelu mykistetty",
+ "thread_muted_and_words": ", sisältää sanat:",
+ "show_full_subject": "Näytä koko otsikko",
+ "hide_full_subject": "Piilota koko otsikko",
+ "show_content": "Näytä sisältö",
+ "hide_content": "Piilota sisältö"
},
"user_card": {
"approve": "Hyväksy",
@@ -561,7 +595,7 @@
"follow": "Seuraa",
"follow_sent": "Pyyntö lähetetty!",
"follow_progress": "Pyydetään…",
- "follow_again": "Lähetä pyyntö uudestaan",
+ "follow_again": "Lähetä pyyntö uudestaan?",
"follow_unfollow": "Älä seuraa",
"followees": "Seuraa",
"followers": "Seuraajat",
@@ -601,7 +635,7 @@
"subscribe": "Tilaa",
"unsubscribe": "Poista tilaus",
"unblock": "Poista esto",
- "unblock_progress": "Postetaan estoa…",
+ "unblock_progress": "Poistetaan estoa…",
"unmute": "Poista mykistys",
"unmute_progress": "Poistetaan mykistystä…",
"mute_progress": "Mykistetään…",
@@ -625,7 +659,8 @@
"user_settings": "Käyttäjäasetukset",
"add_reaction": "Lisää Reaktio",
"accept_follow_request": "Hyväksy seurauspyyntö",
- "reject_follow_request": "Hylkää seurauspyyntö"
+ "reject_follow_request": "Hylkää seurauspyyntö",
+ "bookmark": "Kirjanmerkki"
},
"upload": {
"error": {
@@ -675,7 +710,7 @@
"mute": "Mykistä",
"unmute": "Poista mykistys",
"mute_progress": "Mykistetään…",
- "unmute_progress": "Poistetaan mykistyst…"
+ "unmute_progress": "Poistetaan mykistystä…"
},
"exporter": {
"export": "Vie",
@@ -743,5 +778,8 @@
"people_talking": "{0} käyttäjää puhuvat",
"person_talking": "{0} käyttäjä puhuu",
"no_results": "Ei tuloksia"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma ei voinut käyttää selaimen muistia. Kirjautumisesi ja paikalliset asetukset eivät tallennu ja saatat kohdata odottamattomia ongelmia. Yritä sallia evästeet."
}
}
diff --git a/src/i18n/it.json b/src/i18n/it.json
index ed78e656..24520c09 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -35,7 +35,8 @@
"search": "Ricerca",
"who_to_follow": "Chi seguire",
"preferences": "Preferenze",
- "bookmarks": "Segnalibri"
+ "bookmarks": "Segnalibri",
+ "chats": "Conversazioni"
},
"notifications": {
"followed_you": "ti segue",
@@ -85,7 +86,7 @@
"change_password": "Cambia password",
"change_password_error": "C'è stato un problema durante il cambiamento della password.",
"changed_password": "Password cambiata correttamente!",
- "collapse_subject": "Ripiega messaggi con Oggetto",
+ "collapse_subject": "Ripiega messaggi con oggetto",
"confirm_new_password": "Conferma la nuova password",
"current_password": "La tua password attuale",
"data_import_export_tab": "Importa o esporta dati",
@@ -257,7 +258,12 @@
"panel_header": "Titolo pannello",
"badge_notification": "Notifica",
"popover": "Suggerimenti, menù, sbalzi",
- "toggled": "Scambiato"
+ "toggled": "Scambiato",
+ "chat": {
+ "border": "Bordo",
+ "outgoing": "Inviati",
+ "incoming": "Ricevuti"
+ }
},
"common_colors": {
"rgbo": "Icone, accenti, medaglie",
@@ -398,7 +404,14 @@
"frontend_version": "Versione interfaccia",
"backend_version": "Versione backend",
"title": "Versione"
- }
+ },
+ "reset_avatar": "Azzera icona",
+ "reset_profile_background": "Azzera sfondo profilo",
+ "reset_profile_banner": "Azzera stendardo profilo",
+ "reset_avatar_confirm": "Vuoi veramente azzerare l'icona?",
+ "reset_banner_confirm": "Vuoi veramente azzerare lo stendardo?",
+ "reset_background_confirm": "Vuoi veramente azzerare lo sfondo?",
+ "chatMessageRadius": "Messaggi istantanei"
},
"timeline": {
"error_fetching": "Errore nell'aggiornamento",
@@ -427,7 +440,47 @@
"block": "Blocca",
"blocked": "Bloccato!",
"deny": "Nega",
- "remote_follow": "Segui da remoto"
+ "remote_follow": "Segui da remoto",
+ "admin_menu": {
+ "delete_user_confirmation": "Ne sei completamente sicuro? Quest'azione non può essere annullata.",
+ "delete_user": "Elimina utente",
+ "quarantine": "I messaggi non arriveranno alle altre stanze",
+ "disable_any_subscription": "Rendi utente non seguibile",
+ "disable_remote_subscription": "Blocca i tentativi di seguirlo da altre stanze",
+ "sandbox": "Rendi tutti i messaggi solo per seguaci",
+ "force_unlisted": "Rendi tutti i messaggi invisibili",
+ "strip_media": "Rimuovi ogni allegato ai messaggi",
+ "force_nsfw": "Oscura tutti i messaggi",
+ "delete_account": "Elimina profilo",
+ "deactivate_account": "Disattiva profilo",
+ "activate_account": "Attiva profilo",
+ "revoke_moderator": "Divesti Moderatore",
+ "grant_moderator": "Crea Moderatore",
+ "revoke_admin": "Divesti Amministratore",
+ "grant_admin": "Crea Amministratore",
+ "moderation": "Moderazione"
+ },
+ "show_repeats": "Mostra condivisioni",
+ "hide_repeats": "Nascondi condivisioni",
+ "mute_progress": "Zittisco…",
+ "unmute_progress": "Riabilito…",
+ "unmute": "Riabilita",
+ "block_progress": "Blocco…",
+ "unblock_progress": "Sblocco…",
+ "unblock": "Sblocca",
+ "unsubscribe": "Disdici",
+ "subscribe": "Abbònati",
+ "report": "Segnala",
+ "mention": "Menzioni",
+ "media": "Media",
+ "its_you": "Sei tu!",
+ "hidden": "Nascosto",
+ "follow_unfollow": "Disconosci",
+ "follow_again": "Reinvio richiesta?",
+ "follow_progress": "Richiedo…",
+ "follow_sent": "Richiesta inviata!",
+ "favorites": "Preferiti",
+ "message": "Contatta"
},
"chat": {
"title": "Chat"
@@ -439,7 +492,8 @@
"scope_options": "Opzioni visibilità",
"text_limit": "Lunghezza massima",
"title": "Caratteristiche",
- "who_to_follow": "Chi seguire"
+ "who_to_follow": "Chi seguire",
+ "pleroma_chat_messages": "Chiacchiere"
},
"finder": {
"error_fetching_user": "Errore nel recupero dell'utente",
@@ -493,7 +547,9 @@
"new_status": "Nuovo messaggio",
"empty_status_error": "Non puoi pubblicare messaggi vuoti senza allegati",
"preview_empty": "Vuoto",
- "preview": "Anteprima"
+ "preview": "Anteprima",
+ "media_description_error": "Allegati non caricati, riprova",
+ "media_description": "Descrizione allegati"
},
"registration": {
"bio": "Introduzione",
@@ -517,7 +573,9 @@
"captcha": "CAPTCHA"
},
"user_profile": {
- "timeline_title": "Sequenza dell'Utente"
+ "timeline_title": "Sequenza dell'Utente",
+ "profile_loading_error": "Spiacente, c'è stato un errore nel caricamento del profilo.",
+ "profile_does_not_exist": "Spiacente, questo profilo non esiste."
},
"who_to_follow": {
"more": "Altro",
@@ -626,7 +684,22 @@
"pin": "Intesta al profilo",
"delete": "Elimina messaggio",
"repeats": "Condivisi",
- "favorites": "Preferiti"
+ "favorites": "Preferiti",
+ "hide_content": "Nascondi contenuti",
+ "show_content": "Mostra contenuti",
+ "hide_full_subject": "Nascondi intero oggetto",
+ "show_full_subject": "Mostra intero oggetto",
+ "thread_muted_and_words": ", contiene:",
+ "thread_muted": "Discussione zittita",
+ "copy_link": "Copia collegamento",
+ "status_unavailable": "Messaggio non disponibile",
+ "unmute_conversation": "Riabilita conversazione",
+ "mute_conversation": "Zittisci conversazione",
+ "replies_list": "Risposte:",
+ "reply_to": "Rispondi a",
+ "delete_confirm": "Vuoi veramente eliminare questo messaggio?",
+ "unbookmark": "Rimuovi segnalibro",
+ "bookmark": "Aggiungi segnalibro"
},
"time": {
"years_short": "{0}a",
@@ -661,5 +734,80 @@
"day_short": "{0}g",
"days": "{0} giorni",
"day": "{0} giorno"
+ },
+ "user_reporting": {
+ "title": "Segnalo {0}",
+ "additional_comments": "Osservazioni accessorie",
+ "generic_error": "C'è stato un errore nell'elaborazione della tua richiesta.",
+ "submit": "Invia",
+ "forward_to": "Inoltra a {0}",
+ "forward_description": "Il profilo appartiene ad un'altra stanza. Inviare la segnalazione anche a quella?",
+ "add_comment_description": "La segnalazione sarà inviata ai moderatori della tua stanza. Puoi motivarla qui sotto:"
+ },
+ "password_reset": {
+ "password_reset_required_but_mailer_is_disabled": "Devi reimpostare la tua password, ma non puoi farlo. Contatta il tuo amministratore.",
+ "password_reset_required": "Devi reimpostare la tua password per poter continuare.",
+ "password_reset_disabled": "Non puoi azzerare la tua password. Contatta il tuo amministratore.",
+ "too_many_requests": "Hai raggiunto il numero massimo di tentativi, riprova più tardi.",
+ "not_found": "Non ho trovato questa email o nome utente.",
+ "return_home": "Torna alla pagina principale",
+ "check_email": "Controlla la tua posta elettronica.",
+ "placeholder": "La tua email o nome utente",
+ "instruction": "Inserisci il tuo indirizzo email o il tuo nome utente. Ti invieremo un collegamento per reimpostare la tua password.",
+ "password_reset": "Azzera password",
+ "forgot_password": "Password dimenticata?"
+ },
+ "search": {
+ "no_results": "Nessun risultato",
+ "people_talking": "{count} partecipanti",
+ "person_talking": "{count} partecipante",
+ "hashtags": "Etichette",
+ "people": "Utenti"
+ },
+ "upload": {
+ "file_size_units": {
+ "TiB": "TiB",
+ "GiB": "GiB",
+ "MiB": "MiB",
+ "KiB": "KiB",
+ "B": "B"
+ },
+ "error": {
+ "default": "Riprova in seguito",
+ "file_too_big": "File troppo pesante [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "base": "Caricamento fallito."
+ }
+ },
+ "tool_tip": {
+ "bookmark": "Aggiungi segnalibro",
+ "reject_follow_request": "Rifiuta seguace",
+ "accept_follow_request": "Accetta seguace",
+ "user_settings": "Impostazioni utente",
+ "add_reaction": "Reagisci",
+ "favorite": "Gradisci",
+ "reply": "Rispondi",
+ "repeat": "Ripeti",
+ "media_upload": "Carica allegati"
+ },
+ "display_date": {
+ "today": "Oggi"
+ },
+ "file_type": {
+ "file": "File",
+ "image": "Immagine",
+ "video": "Video",
+ "audio": "Audio"
+ },
+ "chats": {
+ "empty_chat_list_placeholder": "Non hai conversazioni. Contatta qualcuno!",
+ "error_sending_message": "Errore. Il messaggio non è stato inviato.",
+ "error_loading_chat": "Errore. La conversazione non è stata caricata.",
+ "delete_confirm": "Vuoi veramente eliminare questo messaggio?",
+ "more": "Altro",
+ "empty_message_error": "Non puoi inviare messaggi vuoti",
+ "new": "Nuova conversazione",
+ "chats": "Conversazioni",
+ "delete": "Elimina",
+ "message_user": "Contatta {nickname}"
}
}
diff --git a/src/main.js b/src/main.js
index 5bddc76e..0a898022 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,6 +19,7 @@ import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js'
+import chatsModule from './modules/chats.js'
import VueI18n from 'vue-i18n'
@@ -91,7 +92,8 @@ const persistedStateOptions = {
oauthTokens: oauthTokensModule,
reports: reportsModule,
polls: pollsModule,
- postStatus: postStatusModule
+ postStatus: postStatusModule,
+ chats: chatsModule
},
plugins,
strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/api.js b/src/modules/api.js
index 04ef6ab4..68402602 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,4 +1,5 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import { WSConnectionStatus } from '../services/api/api.service.js'
import { Socket } from 'phoenix'
const api = {
@@ -7,6 +8,7 @@ const api = {
fetchers: {},
socket: null,
mastoUserSocket: null,
+ mastoUserSocketStatus: null,
followRequests: []
},
mutations: {
@@ -28,6 +30,9 @@ const api = {
},
setFollowRequests (state, value) {
state.followRequests = value
+ },
+ setMastoUserSocketStatus (state, value) {
+ state.mastoUserSocketStatus = value
}
},
actions: {
@@ -47,7 +52,7 @@ const api = {
startMastoUserSocket (store) {
return new Promise((resolve, reject) => {
try {
- const { state, dispatch, rootState } = store
+ const { state, commit, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
state.mastoUserSocket.addEventListener(
@@ -66,11 +71,22 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
+ } else if (message.event === 'pleroma:chat_update') {
+ dispatch('addChatMessages', {
+ chatId: message.chatUpdate.id,
+ messages: [message.chatUpdate.lastMessage]
+ })
+ dispatch('updateChat', { chat: message.chatUpdate })
}
}
)
+ state.mastoUserSocket.addEventListener('open', () => {
+ commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
+ })
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
+ commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
+ dispatch('clearOpenedChats')
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
const ignoreCodes = new Set([
@@ -84,8 +100,11 @@ const api = {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
+ dispatch('startFetchingChats')
dispatch('restartMastoUserSocket')
}
+ commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
+ dispatch('clearOpenedChats')
})
resolve()
} catch (e) {
@@ -99,12 +118,13 @@ const api = {
return dispatch('startMastoUserSocket').then(() => {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
+ dispatch('stopFetchingChats')
})
},
stopMastoUserSocket ({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
- console.log(state.mastoUserSocket)
+ dispatch('startFetchingChats')
state.mastoUserSocket.close()
},
diff --git a/src/modules/chats.js b/src/modules/chats.js
new file mode 100644
index 00000000..228d6256
--- /dev/null
+++ b/src/modules/chats.js
@@ -0,0 +1,225 @@
+import Vue from 'vue'
+import { find, omitBy, orderBy, sumBy } from 'lodash'
+import chatService from '../services/chat_service/chat_service.js'
+import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
+
+const emptyChatList = () => ({
+ data: [],
+ idStore: {}
+})
+
+const defaultState = {
+ chatList: emptyChatList(),
+ chatListFetcher: null,
+ openedChats: {},
+ openedChatMessageServices: {},
+ fetcher: undefined,
+ currentChatId: null
+}
+
+const getChatById = (state, id) => {
+ return find(state.chatList.data, { id })
+}
+
+const sortedChatList = (state) => {
+ return orderBy(state.chatList.data, ['updated_at'], ['desc'])
+}
+
+const unreadChatCount = (state) => {
+ return sumBy(state.chatList.data, 'unread')
+}
+
+const chats = {
+ state: { ...defaultState },
+ getters: {
+ currentChat: state => state.openedChats[state.currentChatId],
+ currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
+ findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
+ sortedChatList,
+ unreadChatCount
+ },
+ actions: {
+ // Chat list
+ startFetchingChats ({ dispatch, commit }) {
+ const fetcher = () => {
+ dispatch('fetchChats', { latest: true })
+ }
+ fetcher()
+ commit('setChatListFetcher', {
+ fetcher: () => setInterval(() => { fetcher() }, 5000)
+ })
+ },
+ stopFetchingChats ({ commit }) {
+ commit('setChatListFetcher', { fetcher: undefined })
+ },
+ fetchChats ({ dispatch, rootState, commit }, params = {}) {
+ return rootState.api.backendInteractor.chats()
+ .then(({ chats }) => {
+ dispatch('addNewChats', { chats })
+ return chats
+ })
+ },
+ addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
+ commit('addNewChats', { dispatch, chats, rootGetters })
+ },
+ updateChat ({ commit }, { chat }) {
+ commit('updateChat', { chat })
+ },
+
+ // Opened Chats
+ startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
+ dispatch('setCurrentChatFetcher', { fetcher })
+ },
+ setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
+ commit('setCurrentChatFetcher', { fetcher })
+ },
+ addOpenedChat ({ rootState, commit, dispatch }, { chat }) {
+ commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
+ dispatch('addNewUsers', [chat.account])
+ },
+ addChatMessages ({ commit }, value) {
+ commit('addChatMessages', { commit, ...value })
+ },
+ resetChatNewMessageCount ({ commit }, value) {
+ commit('resetChatNewMessageCount', value)
+ },
+ clearCurrentChat ({ rootState, commit, dispatch }, value) {
+ commit('setCurrentChatId', { chatId: undefined })
+ commit('setCurrentChatFetcher', { fetcher: undefined })
+ },
+ readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
+ dispatch('resetChatNewMessageCount')
+ commit('readChat', { id })
+ rootState.api.backendInteractor.readChat({ id, lastReadId })
+ },
+ deleteChatMessage ({ rootState, commit }, value) {
+ rootState.api.backendInteractor.deleteChatMessage(value)
+ commit('deleteChatMessage', { commit, ...value })
+ },
+ resetChats ({ commit, dispatch }) {
+ dispatch('clearCurrentChat')
+ commit('resetChats', { commit })
+ },
+ clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
+ commit('clearOpenedChats', { commit })
+ }
+ },
+ mutations: {
+ setChatListFetcher (state, { commit, fetcher }) {
+ const prevFetcher = state.chatListFetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.chatListFetcher = fetcher && fetcher()
+ },
+ setCurrentChatFetcher (state, { fetcher }) {
+ const prevFetcher = state.fetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.fetcher = fetcher && fetcher()
+ },
+ addOpenedChat (state, { _dispatch, chat }) {
+ state.currentChatId = chat.id
+ Vue.set(state.openedChats, chat.id, chat)
+
+ if (!state.openedChatMessageServices[chat.id]) {
+ Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
+ }
+ },
+ setCurrentChatId (state, { chatId }) {
+ state.currentChatId = chatId
+ },
+ addNewChats (state, { _dispatch, chats, _rootGetters }) {
+ chats.forEach((updatedChat) => {
+ const chat = getChatById(state, updatedChat.id)
+
+ if (chat) {
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
+ } else {
+ state.chatList.data.push(updatedChat)
+ Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ }
+ })
+ },
+ updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
+ const chat = getChatById(state, updatedChat.id)
+ if (chat) {
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
+ chat.updated_at = updatedChat.updated_at
+ }
+ if (!chat) { state.chatList.data.unshift(updatedChat) }
+ Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ },
+ deleteChat (state, { _dispatch, id, _rootGetters }) {
+ state.chats.data = state.chats.data.filter(conversation =>
+ conversation.last_status.id !== id
+ )
+ state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
+ },
+ resetChats (state, { commit }) {
+ state.chatList = emptyChatList()
+ state.currentChatId = null
+ commit('setChatListFetcher', { fetcher: undefined })
+ for (const chatId in state.openedChats) {
+ chatService.clear(state.openedChatMessageServices[chatId])
+ Vue.delete(state.openedChats, chatId)
+ Vue.delete(state.openedChatMessageServices, chatId)
+ }
+ },
+ setChatsLoading (state, { value }) {
+ state.chats.loading = value
+ },
+ addChatMessages (state, { commit, chatId, messages }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
+ commit('refreshLastMessage', { chatId })
+ }
+ },
+ refreshLastMessage (state, { chatId }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ const chat = getChatById(state, chatId)
+ if (chat) {
+ chat.lastMessage = chatMessageService.lastMessage
+ if (chatMessageService.lastMessage) {
+ chat.updated_at = chatMessageService.lastMessage.created_at
+ }
+ }
+ }
+ },
+ deleteChatMessage (state, { commit, chatId, messageId }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.deleteMessage(chatMessageService, messageId)
+ commit('refreshLastMessage', { chatId })
+ }
+ },
+ resetChatNewMessageCount (state, _value) {
+ const chatMessageService = state.openedChatMessageServices[state.currentChatId]
+ chatService.resetNewMessageCount(chatMessageService)
+ },
+ // Used when a connection loss occurs
+ clearOpenedChats (state) {
+ const currentChatId = state.currentChatId
+ for (const chatId in state.openedChats) {
+ if (currentChatId !== chatId) {
+ chatService.clear(state.openedChatMessageServices[chatId])
+ Vue.delete(state.openedChats, chatId)
+ Vue.delete(state.openedChatMessageServices, chatId)
+ }
+ }
+ },
+ readChat (state, { id }) {
+ const chat = getChatById(state, id)
+ if (chat) {
+ chat.unread = 0
+ }
+ }
+ }
+}
+
+export default chats
diff --git a/src/modules/config.js b/src/modules/config.js
index 47b24d77..e0fe72df 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -46,7 +46,8 @@ export const defaultState = {
repeats: true,
moves: true,
emojiReactions: false,
- followRequest: true
+ followRequest: true,
+ chatMention: true
},
webPushNotifications: false,
muteWords: [],
diff --git a/src/modules/instance.js b/src/modules/instance.js
index ec5f4e54..3fe3bbf3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -15,6 +15,8 @@ const defaultState = {
// Stuff from static/config.json
alwaysShowSubjectInput: true,
+ defaultAvatar: '/images/avi.png',
+ defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg',
collapseMessageWithSubject: false,
disableChat: false,
@@ -53,6 +55,7 @@ const defaultState = {
// Feature-set, apparently, not everything here is reported...
chatAvailable: false,
+ pleromaChatMessagesAvailable: false,
gopherAvailable: false,
mediaProxyAvailable: false,
suggestionsEnabled: false,
diff --git a/src/modules/interface.js b/src/modules/interface.js
index e31630fc..ec08ac0a 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -15,7 +15,8 @@ const defaultState = {
)
},
mobileLayout: false,
- globalNotices: []
+ globalNotices: [],
+ layoutHeight: 0
}
const interfaceMod = {
@@ -65,6 +66,9 @@ const interfaceMod = {
},
removeGlobalNotice (state, notice) {
state.globalNotices = state.globalNotices.filter(n => n !== notice)
+ },
+ setLayoutHeight (state, value) {
+ state.layoutHeight = value
}
},
actions: {
@@ -110,6 +114,9 @@ const interfaceMod = {
},
removeGlobalNotice ({ commit }, notice) {
commit('removeGlobalNotice', notice)
+ },
+ setLayoutHeight ({ commit }, value) {
+ commit('setLayoutHeight', value)
}
}
}
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 7fbf685c..64f5b587 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -478,7 +478,7 @@ export const mutations = {
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
- newStatus.deleted = true
+ if (newStatus) newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
@@ -521,6 +521,9 @@ export const mutations = {
dismissNotification (state, { id }) {
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
},
+ dismissNotifications (state, { finder }) {
+ state.notifications.data = state.notifications.data.filter(n => finder)
+ },
updateNotification (state, { id, updater }) {
const notification = find(state.notifications.data, n => n.id === id)
notification && updater(notification)
diff --git a/src/modules/users.js b/src/modules/users.js
index 7e136c61..16c1e566 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -498,6 +498,7 @@ const users = {
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
+ store.dispatch('resetChats')
})
},
loginUser (store, accessToken) {
@@ -537,6 +538,9 @@ const users = {
// Start fetching notifications
store.dispatch('startFetchingNotifications')
+
+ // Start fetching chats
+ store.dispatch('startFetchingChats')
}
if (store.getters.mergedConfig.useStreamingApi) {
@@ -544,6 +548,7 @@ const users = {
console.error('Failed initializing MastoAPI Streaming socket', error)
startPolling()
}).then(() => {
+ store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
})
} else {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ad543c6c..40ea5bd9 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
@@ -81,6 +81,11 @@ const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
+const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
+const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
+const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
+const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
+const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const oldfetch = window.fetch
@@ -141,20 +146,11 @@ const updateNotificationSettings = ({ credentials, settings }) => {
}).then((data) => data.json())
}
-const updateAvatar = ({ credentials, avatar }) => {
+const updateProfileImages = ({ credentials, avatar = null, banner = null, background = null }) => {
const form = new FormData()
- form.append('avatar', avatar)
- return fetch(MASTODON_PROFILE_UPDATE_URL, {
- headers: authHeaders(credentials),
- method: 'PATCH',
- body: form
- }).then((data) => data.json())
- .then((data) => parseUser(data))
-}
-
-const updateBg = ({ credentials, background }) => {
- const form = new FormData()
- form.append('pleroma_background_image', background)
+ if (avatar !== null) form.append('avatar', avatar)
+ if (banner !== null) form.append('header', banner)
+ if (background !== null) form.append('pleroma_background_image', background)
return fetch(MASTODON_PROFILE_UPDATE_URL, {
headers: authHeaders(credentials),
method: 'PATCH',
@@ -164,17 +160,6 @@ const updateBg = ({ credentials, background }) => {
.then((data) => parseUser(data))
}
-const updateBanner = ({ credentials, banner }) => {
- const form = new FormData()
- form.append('header', banner)
- return fetch(MASTODON_PROFILE_UPDATE_URL, {
- headers: authHeaders(credentials),
- method: 'PATCH',
- body: form
- }).then((data) => data.json())
- .then((data) => parseUser(data))
-}
-
const updateProfile = ({ credentials, params }) => {
return promisedRequest({
url: MASTODON_PROFILE_UPDATE_URL,
@@ -1087,6 +1072,10 @@ const MASTODON_STREAMING_EVENTS = new Set([
'filters_changed'
])
+const PLEROMA_STREAMING_EVENTS = new Set([
+ 'pleroma:chat_update'
+])
+
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
// Uses EventTarget and a CustomEvent to proxy events
export const ProcessedWS = ({
@@ -1143,7 +1132,7 @@ export const handleMastoWS = (wsEvent) => {
if (!data) return
const parsedEvent = JSON.parse(data)
const { event, payload } = parsedEvent
- if (MASTODON_STREAMING_EVENTS.has(event)) {
+ if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) {
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
if (event === 'delete') {
return { event, id: payload }
@@ -1153,6 +1142,8 @@ export const handleMastoWS = (wsEvent) => {
return { event, status: parseStatus(data) }
} else if (event === 'notification') {
return { event, notification: parseNotification(data) }
+ } else if (event === 'pleroma:chat_update') {
+ return { event, chatUpdate: parseChat(data) }
}
} else {
console.warn('Unknown event', wsEvent)
@@ -1160,6 +1151,81 @@ export const handleMastoWS = (wsEvent) => {
}
}
+export const WSConnectionStatus = Object.freeze({
+ 'JOINED': 1,
+ 'CLOSED': 2,
+ 'ERROR': 3
+})
+
+const chats = ({ credentials }) => {
+ return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) })
+ .then((data) => data.json())
+ .then((data) => {
+ return { chats: data.map(parseChat).filter(c => c) }
+ })
+}
+
+const getOrCreateChat = ({ accountId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_URL(accountId),
+ method: 'POST',
+ credentials
+ })
+}
+
+const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
+ let url = PLEROMA_CHAT_MESSAGES_URL(id)
+ const args = [
+ maxId && `max_id=${maxId}`,
+ sinceId && `since_id=${sinceId}`,
+ limit && `limit=${limit}`
+ ].filter(_ => _).join('&')
+
+ url = url + (args ? '?' + args : '')
+
+ return promisedRequest({
+ url,
+ method: 'GET',
+ credentials
+ })
+}
+
+const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
+ const payload = {
+ 'content': content
+ }
+
+ if (mediaId) {
+ payload['media_id'] = mediaId
+ }
+
+ return promisedRequest({
+ url: PLEROMA_CHAT_MESSAGES_URL(id),
+ method: 'POST',
+ payload: payload,
+ credentials
+ })
+}
+
+const readChat = ({ id, lastReadId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_READ_URL(id),
+ method: 'POST',
+ payload: {
+ 'last_read_id': lastReadId
+ },
+ credentials
+ })
+}
+
+const deleteChatMessage = ({ chatId, messageId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
+ method: 'DELETE',
+ credentials
+ })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1206,10 +1272,8 @@ const apiService = {
deactivateUser,
register,
getCaptcha,
- updateAvatar,
- updateBg,
+ updateProfileImages,
updateProfile,
- updateBanner,
importBlocks,
importFollows,
deleteAccount,
@@ -1240,7 +1304,13 @@ const apiService = {
fetchKnownDomains,
fetchDomainMutes,
muteDomain,
- unmuteDomain
+ unmuteDomain,
+ chats,
+ getOrCreateChat,
+ chatMessages,
+ sendChatMessage,
+ readChat,
+ deleteChatMessage
}
export default apiService
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
new file mode 100644
index 00000000..b60a889b
--- /dev/null
+++ b/src/services/chat_service/chat_service.js
@@ -0,0 +1,151 @@
+import _ from 'lodash'
+
+const empty = (chatId) => {
+ return {
+ idIndex: {},
+ messages: [],
+ newMessageCount: 0,
+ lastSeenTimestamp: 0,
+ chatId: chatId,
+ minId: undefined,
+ lastMessage: undefined
+ }
+}
+
+const clear = (storage) => {
+ storage.idIndex = {}
+ storage.messages.splice(0, storage.messages.length)
+ storage.newMessageCount = 0
+ storage.lastSeenTimestamp = 0
+ storage.minId = undefined
+ storage.lastMessage = undefined
+}
+
+const deleteMessage = (storage, messageId) => {
+ if (!storage) { return }
+ storage.messages = storage.messages.filter(m => m.id !== messageId)
+ delete storage.idIndex[messageId]
+
+ if (storage.lastMessage && (storage.lastMessage.id === messageId)) {
+ storage.lastMessage = _.maxBy(storage.messages, 'id')
+ }
+
+ if (storage.minId === messageId) {
+ const firstMessage = _.minBy(storage.messages, 'id')
+ storage.minId = firstMessage.id
+ }
+}
+
+const add = (storage, { messages: newMessages }) => {
+ if (!storage) { return }
+ for (let i = 0; i < newMessages.length; i++) {
+ const message = newMessages[i]
+
+ // sanity check
+ if (message.chat_id !== storage.chatId) { return }
+
+ if (!storage.minId || message.id < storage.minId) {
+ storage.minId = message.id
+ }
+
+ if (!storage.lastMessage || message.id > storage.lastMessage.id) {
+ storage.lastMessage = message
+ }
+
+ if (!storage.idIndex[message.id]) {
+ if (storage.lastSeenTimestamp < message.created_at) {
+ storage.newMessageCount++
+ }
+ storage.messages.push(message)
+ storage.idIndex[message.id] = message
+ }
+ }
+}
+
+const resetNewMessageCount = (storage) => {
+ if (!storage) { return }
+ storage.newMessageCount = 0
+ storage.lastSeenTimestamp = new Date()
+}
+
+// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
+const getView = (storage) => {
+ if (!storage) { return [] }
+
+ const result = []
+ const messages = _.sortBy(storage.messages, ['id', 'desc'])
+ const firstMessage = messages[0]
+ let previousMessage = messages[messages.length - 1]
+ let currentMessageChainId
+
+ if (firstMessage) {
+ const date = new Date(firstMessage.created_at)
+ date.setHours(0, 0, 0, 0)
+ result.push({
+ type: 'date',
+ date,
+ id: date.getTime().toString()
+ })
+ }
+
+ let afterDate = false
+
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i]
+ const nextMessage = messages[i + 1]
+
+ const date = new Date(message.created_at)
+ date.setHours(0, 0, 0, 0)
+
+ // insert date separator and start a new message chain
+ if (previousMessage && previousMessage.date < date) {
+ result.push({
+ type: 'date',
+ date,
+ id: date.getTime().toString()
+ })
+
+ previousMessage['isTail'] = true
+ currentMessageChainId = undefined
+ afterDate = true
+ }
+
+ const object = {
+ type: 'message',
+ data: message,
+ date,
+ id: message.id,
+ messageChainId: currentMessageChainId
+ }
+
+ // end a message chian
+ if ((nextMessage && nextMessage.account_id) !== message.account_id) {
+ object['isTail'] = true
+ currentMessageChainId = undefined
+ }
+
+ // start a new message chain
+ if ((previousMessage && previousMessage.data && previousMessage.data.account_id) !== message.account_id || afterDate) {
+ currentMessageChainId = _.uniqueId()
+ object['isHead'] = true
+ object['messageChainId'] = currentMessageChainId
+ }
+
+ result.push(object)
+ previousMessage = object
+ afterDate = false
+ }
+
+ return result
+}
+
+const ChatService = {
+ add,
+ empty,
+ getView,
+ deleteMessage,
+ resetNewMessageCount,
+ clear
+}
+
+export default ChatService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ec83c02a..7ea8a16c 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -183,6 +183,7 @@ export const parseUser = (data) => {
output.deactivated = data.pleroma.deactivated
output.notification_settings = data.pleroma.notification_settings
+ output.unread_chat_count = data.pleroma.unread_chat_count
}
output.tags = output.tags || []
@@ -372,7 +373,7 @@ export const parseNotification = (data) => {
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.action = parsedNotice
- output.from_profile = parseUser(data.from_profile)
+ output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
}
output.created_at = new Date(data.created_at)
@@ -398,3 +399,34 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
minId: flakeId ? minId : parseInt(minId, 10)
}
}
+
+export const parseChat = (chat) => {
+ const output = {}
+ output.id = chat.id
+ output.account = parseUser(chat.account)
+ output.unread = chat.unread
+ output.lastMessage = parseChatMessage(chat.last_message)
+ output.updated_at = new Date(chat.updated_at)
+ return output
+}
+
+export const parseChatMessage = (message) => {
+ if (!message) { return }
+ if (message.isNormalized) { return message }
+ const output = message
+ output.id = message.id
+ output.created_at = new Date(message.created_at)
+ output.chat_id = message.chat_id
+ if (message.content) {
+ output.content = addEmojis(message.content, message.emojis)
+ } else {
+ output.content = ''
+ }
+ if (message.attachment) {
+ output.attachments = [parseAttachment(message.attachment)]
+ } else {
+ output.attachments = []
+ }
+ output.isNormalized = true
+ return output
+}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index fbdcf562..07425abd 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -106,7 +106,8 @@ export const generateRadii = (input) => {
avatar: 5,
avatarAlt: 50,
tooltip: 2,
- attachment: 5
+ attachment: 5,
+ chatMessage: inputRadii.panel
})
return {
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 6b25cd6f..b58ca9be 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -23,7 +23,9 @@ export const LAYERS = {
inputTopBar: 'topBar',
alert: 'bg',
alertPanel: 'panel',
- poll: 'bg'
+ poll: 'bg',
+ chatBg: 'underlay',
+ chatMessage: 'chatBg'
}
/* By default opacity slots have 1 as default opacity
@@ -667,5 +669,54 @@ export const SLOT_INHERITANCE = {
layer: 'badge',
variant: 'badgeNotification',
textColor: 'bw'
+ },
+
+ chatBg: {
+ depends: ['bg']
+ },
+
+ chatMessage: {
+ depends: ['chatBg']
+ },
+
+ chatMessageIncomingBg: {
+ depends: ['chatMessage'],
+ layer: 'chatMessage'
+ },
+
+ chatMessageIncomingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageIncomingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageIncomingBorder: {
+ depends: ['border'],
+ opacity: 'border',
+ color: (mod, border) => brightness(2 * mod, border).rgb
+ },
+
+ chatMessageOutgoingBg: {
+ depends: ['chatMessage'],
+ color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
+ },
+
+ chatMessageOutgoingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageOutgoingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageOutgoingBorder: {
+ depends: ['chatMessage'],
+ opacity: 'chatMessage'
}
}
diff --git a/src/services/window_utils/window_utils.js b/src/services/window_utils/window_utils.js
index faff6cb9..909088db 100644
--- a/src/services/window_utils/window_utils.js
+++ b/src/services/window_utils/window_utils.js
@@ -3,3 +3,8 @@ export const windowWidth = () =>
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
+
+export const windowHeight = () =>
+ window.innerHeight ||
+ document.documentElement.clientHeight ||
+ document.body.clientHeight
diff --git a/static/fontello.json b/static/fontello.json
index 5ef8544e..706800cd 100644
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -399,6 +399,12 @@
"css": "doc",
"code": 59433,
"src": "fontawesome"
+ },
+ {
+ "uid": "98d9c83c1ee7c2c25af784b518c522c5",
+ "css": "block",
+ "code": 59434,
+ "src": "fontawesome"
}
]
}
\ No newline at end of file
diff --git a/static/terms-of-service.html b/static/terms-of-service.html
index b2c66815..3b6bbb36 100644
--- a/static/terms-of-service.html
+++ b/static/terms-of-service.html
@@ -2,7 +2,7 @@
This is the default placeholder ToS. You should copy it over to your static folder and edit it to fit the needs of your instance.
-
To do so, place a file at "/instance/static/terms-of-service.html"
in your
+
To do so, place a file at "/instance/static/static/terms-of-service.html"
in your
Pleroma install containing the real ToS for your instance.
See the Pleroma documentation for more information.
diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js
index a415aeaf..3673256f 100644
--- a/test/unit/specs/boot/routes.spec.js
+++ b/test/unit/specs/boot/routes.spec.js
@@ -1,14 +1,22 @@
+import Vuex from 'vuex'
import routes from 'src/boot/routes'
import { createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
+localVue.use(Vuex)
localVue.use(VueRouter)
+const store = new Vuex.Store({
+ state: {
+ instance: {}
+ }
+})
+
describe('routes', () => {
const router = new VueRouter({
mode: 'abstract',
- routes: routes({})
+ routes: routes(store)
})
it('root path', () => {
diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js
new file mode 100644
index 00000000..4e8e566b
--- /dev/null
+++ b/test/unit/specs/services/chat_service/chat_service.spec.js
@@ -0,0 +1,89 @@
+import chatService from '../../../../../src/services/chat_service/chat_service.js'
+
+const message1 = {
+ id: '9wLkdcmQXD21Oy8lEX',
+ created_at: (new Date('2020-06-22T18:45:53.000Z'))
+}
+
+const message2 = {
+ id: '9wLkdp6ihaOVdNj8Wu',
+ account_id: '9vmRb29zLQReckr5ay',
+ created_at: (new Date('2020-06-22T18:45:56.000Z'))
+}
+
+const message3 = {
+ id: '9wLke9zL4Dy4OZR2RM',
+ account_id: '9vmRb29zLQReckr5ay',
+ created_at: (new Date('2020-07-22T18:45:59.000Z'))
+}
+
+// TODO: only
+describe.only('chatService', () => {
+ describe('.add', () => {
+ it("Doesn't add duplicates", () => {
+ const chat = chatService.empty()
+ chatService.add(chat, { messages: [ message1 ] })
+ chatService.add(chat, { messages: [ message1 ] })
+ expect(chat.messages.length).to.eql(1)
+
+ chatService.add(chat, { messages: [ message2 ] })
+ expect(chat.messages.length).to.eql(2)
+ })
+
+ it('Updates minId and lastMessage and newMessageCount', () => {
+ const chat = chatService.empty()
+
+ chatService.add(chat, { messages: [ message1 ] })
+ expect(chat.lastMessage.id).to.eql(message1.id)
+ expect(chat.minId).to.eql(message1.id)
+ expect(chat.newMessageCount).to.eql(1)
+
+ chatService.add(chat, { messages: [ message2 ] })
+ expect(chat.lastMessage.id).to.eql(message2.id)
+ expect(chat.minId).to.eql(message1.id)
+ expect(chat.newMessageCount).to.eql(2)
+
+ chatService.resetNewMessageCount(chat)
+ expect(chat.newMessageCount).to.eql(0)
+
+ const createdAt = new Date()
+ createdAt.setSeconds(createdAt.getSeconds() + 10)
+ chatService.add(chat, { messages: [ { message3, created_at: createdAt } ] })
+ expect(chat.newMessageCount).to.eql(1)
+ })
+ })
+
+ describe('.delete', () => {
+ it('Updates minId and lastMessage', () => {
+ const chat = chatService.empty()
+
+ chatService.add(chat, { messages: [ message1 ] })
+ chatService.add(chat, { messages: [ message2 ] })
+ chatService.add(chat, { messages: [ message3 ] })
+
+ expect(chat.lastMessage.id).to.eql(message3.id)
+ expect(chat.minId).to.eql(message1.id)
+
+ chatService.deleteMessage(chat, message3.id)
+ expect(chat.lastMessage.id).to.eql(message2.id)
+ expect(chat.minId).to.eql(message1.id)
+
+ chatService.deleteMessage(chat, message1.id)
+ expect(chat.lastMessage.id).to.eql(message2.id)
+ expect(chat.minId).to.eql(message2.id)
+ })
+ })
+
+ describe('.getView', () => {
+ it('Inserts date separators', () => {
+ const chat = chatService.empty()
+
+ chatService.add(chat, { messages: [ message1 ] })
+ chatService.add(chat, { messages: [ message2 ] })
+ chatService.add(chat, { messages: [ message3 ] })
+
+ const view = chatService.getView(chat)
+ expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message'])
+ })
+ })
+})