diff --git a/src/App.scss b/src/App.scss index 598735d9..244b3474 100644 --- a/src/App.scss +++ b/src/App.scss @@ -154,7 +154,7 @@ input, textarea, .select { background: transparent; border: none; color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--inputText, --text, $fallback--text); margin: 0; padding: 0 2em 0 .2em; font-family: sans-serif; diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 0a4ec857..f5e84cbc 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -97,6 +97,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('showInstanceSpecificPanel') copyInstanceOption('scopeOptionsEnabled') copyInstanceOption('formattingOptionsEnabled') + copyInstanceOption('hideMutedPosts') copyInstanceOption('collapseMessageWithSubject') copyInstanceOption('loginMethod') copyInstanceOption('scopeCopy') @@ -203,6 +204,12 @@ const getNodeInfo = async ({ store }) => { const suggestions = metadata.suggestions store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) + + const software = data.software + store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) + + const frontendVersion = window.___pleromafe_commit_hash + store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) } else { throw (res) } diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 9b80c72b..8afe8b44 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -8,8 +8,8 @@
- - {{ user.name }} + + {{ user.name }}
@@ -52,6 +52,14 @@ width: 16px; vertical-align: middle; } + + &-value { + display: inline-block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } } &-expanded-content { diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js index 8f1ac3d9..1da70ce9 100644 --- a/src/components/conversation-page/conversation-page.js +++ b/src/components/conversation-page/conversation-page.js @@ -1,5 +1,4 @@ import Conversation from '../conversation/conversation.vue' -import { find } from 'lodash' const conversationPage = { components: { @@ -8,8 +7,8 @@ const conversationPage = { computed: { statusoid () { const id = this.$route.params.id - const statuses = this.$store.state.statuses.allStatuses - const status = find(statuses, {id}) + const statuses = this.$store.state.statuses.allStatusesObject + const status = statuses[id] return status } diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 48b8aaaa..e806be8e 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,4 +1,5 @@ import { reduce, filter } from 'lodash' +import { set } from 'vue' import Status from '../status/status.vue' const sortById = (a, b) => { @@ -25,7 +26,8 @@ const sortAndFilterConversation = (conversation) => { const conversation = { data () { return { - highlight: null + highlight: null, + converationStatusIds: [] } }, props: [ @@ -36,6 +38,15 @@ const conversation = { status () { return this.statusoid }, + idsToShow () { + if (this.converationStatusIds.length > 0) { + return this.converationStatusIds + } else if (this.statusId) { + return [this.statusId] + } else { + return [] + } + }, statusId () { if (this.statusoid.retweeted_status) { return this.statusoid.retweeted_status.id @@ -48,9 +59,11 @@ const conversation = { return [] } - const conversationId = this.status.statusnet_conversation_id - const statuses = this.$store.state.statuses.allStatuses - const conversation = filter(statuses, { statusnet_conversation_id: conversationId }) + const statusesObject = this.$store.state.statuses.allStatusesObject + const conversation = this.idsToShow.reduce((acc, id) => { + acc.push(statusesObject[id]) + return acc + }, []) return sortAndFilterConversation(conversation) }, replies () { @@ -83,9 +96,15 @@ const conversation = { methods: { fetchConversation () { if (this.status) { - const conversationId = this.status.statusnet_conversation_id - this.$store.state.api.backendInteractor.fetchConversation({id: conversationId}) - .then((statuses) => this.$store.dispatch('addNewStatuses', { statuses })) + this.$store.state.api.backendInteractor.fetchConversation({id: this.status.id}) + .then(({ancestors, descendants}) => { + this.$store.dispatch('addNewStatuses', { statuses: ancestors }) + this.$store.dispatch('addNewStatuses', { statuses: descendants }) + set(this, 'converationStatusIds', [].concat( + ancestors.map(_ => _.id), + this.statusId, + descendants.map(_ => _.id))) + }) .then(() => this.setHighlight(this.statusId)) } else { const id = this.$route.params.id diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index 425c9c3e..ac4e265a 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -1,4 +1,5 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import RemoteFollow from '../remote_follow/remote_follow.vue' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' const FollowCard = { @@ -14,13 +15,17 @@ const FollowCard = { } }, components: { - BasicUserCard + BasicUserCard, + RemoteFollow }, computed: { isMe () { return this.$store.state.users.currentUser.id === this.user.id }, following () { return this.updated ? this.updated.following : this.user.following }, showFollow () { return !this.following || this.updated && !this.updated.following + }, + loggedIn () { + return this.$store.state.users.currentUser } }, methods: { diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index 6cb064eb..9f314fd3 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -4,9 +4,12 @@ {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }} + -
@@ -287,8 +287,6 @@ img { width: 24px; height: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); object-fit: contain; } diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index ab6cd64d..8dc00420 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -35,6 +35,9 @@ const registration = { }, computed: { token () { return this.$route.params.token }, + bioPlaceholder () { + return this.$t('registration.bio_placeholder').replace(/\s*\n\s*/g, ' \n') + }, ...mapState({ registrationOpen: (state) => state.instance.registrationOpen, signedIn: (state) => !!state.users.currentUser, diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index e22b308d..110b27bf 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -45,7 +45,7 @@
- +
diff --git a/src/components/remote_follow/remote_follow.js b/src/components/remote_follow/remote_follow.js new file mode 100644 index 00000000..461d58c9 --- /dev/null +++ b/src/components/remote_follow/remote_follow.js @@ -0,0 +1,10 @@ +export default { + props: [ 'user' ], + computed: { + subscribeUrl () { + // eslint-disable-next-line no-undef + const serverUrl = new URL(this.user.statusnet_profile_url) + return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` + } + } +} diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue new file mode 100644 index 00000000..fb2147bd --- /dev/null +++ b/src/components/remote_follow/remote_follow.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 979457a5..1d5f75ed 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,8 +1,13 @@ /* eslint-env browser */ +import { filter, trim } from 'lodash' + import TabSwitcher from '../tab_switcher/tab_switcher.js' import StyleSwitcher from '../style_switcher/style_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' -import { filter, trim } from 'lodash' +import { extractCommit } from '../../services/version/version.service' + +const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' +const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' const settings = { data () { @@ -42,6 +47,11 @@ const settings = { pauseOnUnfocusedLocal: user.pauseOnUnfocused, hoverPreviewLocal: user.hoverPreview, + hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined' + ? instance.hideMutedPosts + : user.hideMutedPosts, + hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts), + collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined' ? instance.collapseMessageWithSubject : user.collapseMessageWithSubject, @@ -78,7 +88,10 @@ const settings = { // Future spec, still not supported in Nightly 63 as of 08/2018 Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'), playVideosInModal: user.playVideosInModal, - useContainFit: user.useContainFit + useContainFit: user.useContainFit, + + backendVersion: instance.backendVersion, + frontendVersion: instance.frontendVersion } }, components: { @@ -96,7 +109,13 @@ const settings = { postFormats () { return this.$store.state.instance.postFormats || [] }, - instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel } + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + frontendVersionLink () { + return pleromaFeCommitUrl + this.frontendVersion + }, + backendVersionLink () { + return pleromaBeCommitUrl + extractCommit(this.backendVersion) + } }, watch: { hideAttachmentsLocal (value) { @@ -163,6 +182,9 @@ const settings = { value = filter(value.split('\n'), (word) => trim(word).length > 0) this.$store.dispatch('setOption', { name: 'muteWords', value }) }, + hideMutedPostsLocal (value) { + this.$store.dispatch('setOption', { name: 'hideMutedPosts', value }) + }, collapseMessageWithSubjectLocal (value) { this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value }) }, diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index d2346747..33dad549 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -36,6 +36,10 @@

{{$t('nav.timeline')}}

    +
  • + + +
+
+
+ +
+
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 43a77f45..197c61d5 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,4 +1,5 @@ import UserAvatar from '../user_avatar/user_avatar.vue' +import RemoteFollow from '../remote_follow/remote_follow.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -99,7 +100,8 @@ export default { } }, components: { - UserAvatar + UserAvatar, + RemoteFollow }, methods: { followUser () { @@ -119,24 +121,16 @@ export default { }) }, blockUser () { - const store = this.$store - store.state.api.backendInteractor.blockUser(this.user.id) - .then((blockedUser) => { - store.commit('addNewUsers', [blockedUser]) - store.commit('removeStatus', { timeline: 'friends', userId: this.user.id }) - store.commit('removeStatus', { timeline: 'public', userId: this.user.id }) - store.commit('removeStatus', { timeline: 'publicAndExternal', userId: this.user.id }) - }) + this.$store.dispatch('blockUser', this.user.id) }, unblockUser () { - const store = this.$store - store.state.api.backendInteractor.unblockUser(this.user.id) - .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser])) + this.$store.dispatch('unblockUser', this.user.id) }, - toggleMute () { - const store = this.$store - store.commit('setMuted', {user: this.user, muted: !this.user.muted}) - store.state.api.backendInteractor.setUserMute(this.user) + muteUser () { + this.$store.dispatch('muteUser', this.user.id) + }, + unmuteUser () { + this.$store.dispatch('unmuteUser', this.user.id) }, setProfileView (v) { if (this.switcher) { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 690e1bde..3259d1c5 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -74,24 +74,18 @@
- -
-
-
- - - -
+
+
@@ -375,11 +369,6 @@ min-height: 28px; } - .remote-follow { - max-width: 220px; - min-height: 28px; - } - .follow { max-width: 220px; min-height: 28px; diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index c0ab759c..72e7bb53 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -158,7 +158,13 @@ const UserSettings = { reader.readAsDataURL(file) }, submitAvatar (cropper, file) { - const img = cropper.getCroppedCanvas().toDataURL(file.type) + let img + if (cropper) { + img = cropper.getCroppedCanvas().toDataURL(file.type) + } else { + img = file + } + return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => { if (!user.error) { this.$store.commit('addNewUsers', [user]) diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index a1123638..c9e68808 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -192,6 +192,12 @@
+ +
+ + + +
diff --git a/src/i18n/en.json b/src/i18n/en.json index 01fe2fba..c501c6a7 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -25,6 +25,7 @@ "image_cropper": { "crop_picture": "Crop picture", "save": "Save", + "save_without_cropping": "Save without cropping", "cancel": "Cancel" }, "login": { @@ -97,7 +98,7 @@ "new_captcha": "Click the image to get a new captcha", "username_placeholder": "e.g. lain", "fullname_placeholder": "e.g. Lain Iwakura", - "bio_placeholder": "e.g.\nHi, I'm Lain\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", + "bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", "validations": { "username_required": "cannot be left blank", "fullname_required": "cannot be left blank", @@ -152,6 +153,7 @@ "general": "General", "hide_attachments_in_convo": "Hide attachments in conversations", "hide_attachments_in_tl": "Hide attachments in timeline", + "hide_muted_posts": "Hide posts of muted users", "max_thumbnails": "Maximum amount of thumbnails per post", "hide_isp": "Hide instance-specific panel", "preload_images": "Preload images", @@ -347,6 +349,11 @@ "checkbox": "I have skimmed over terms and conditions", "link": "a nice lil' link" } + }, + "version": { + "title": "Version", + "backend_version": "Backend Version", + "frontend_version": "Frontend Version" } }, "timeline": { diff --git a/src/i18n/oc.json b/src/i18n/oc.json index baac3d25..ecc4df61 100644 --- a/src/i18n/oc.json +++ b/src/i18n/oc.json @@ -59,19 +59,21 @@ "broken_favorite": "Estatut desconegut, sèm a lo cercar...", "favorited_you": "a aimat vòstre estatut", "followed_you": "vos a seguit", - "load_older": "Cargar las notificaciones mai ancianas", + "load_older": "Cargar las notificacions mai ancianas", "notifications": "Notficacions", - "read": "Legit !", + "read": "Legit !", "repeated_you": "a repetit vòstre estatut", "no_more_notifications": "Pas mai de notificacions" }, "post_status": { "new_status": "Publicar d’estatuts novèls", - "account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu'a vòstres seguidors.", + "account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu’a vòstres seguidors.", "account_not_locked_warning_link": "clavat", "attachments_sensitive": "Marcar las pèças juntas coma sensiblas", "content_type": { - "text/plain": "Tèxte brut" + "text/plain": "Tèxte brut", + "text/html": "HTML", + "text/markdown": "Markdown" }, "content_warning": "Avís de contengut (opcional)", "default": "Escrivètz aquí vòstre estatut.", @@ -118,12 +120,12 @@ "blocks_tab": "Blocatges", "btnRadius": "Botons", "cBlue": "Blau (Respondre, seguir)", - "cGreen": "Verd (Repartajar)", + "cGreen": "Verd (Repertir)", "cOrange": "Irange (Aimar)", "cRed": "Roge (Anullar)", "change_password": "Cambiar lo senhal", "change_password_error": "Una error s’es producha en cambiant lo senhal.", - "changed_password": "Senhal corrèctament cambiat !", + "changed_password": "Senhal corrèctament cambiat !", "collapse_subject": "Replegar las publicacions amb de subjèctes", "composing": "Escritura", "confirm_new_password": "Confirmatz lo nòu senhal", @@ -134,7 +136,7 @@ "default_vis": "Nivèl de visibilitat per defaut", "delete_account": "Suprimir lo compte", "delete_account_description": "Suprimir vòstre compte e los messatges per sempre.", - "delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrador d’instància.", + "delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrator d’instància.", "delete_account_instructions": "Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.", "avatar_size_instruction": "La talha minimum recomandada pels imatges d’avatar es 150x150 pixèls.", "export_theme": "Enregistrar la preconfiguracion", @@ -154,14 +156,14 @@ "hide_isp": "Amagar lo panèl especial instància", "preload_images": "Precargar los imatges", "use_one_click_nsfw": "Dobrir las pèças juntas NSFW amb un clic", - "hide_post_stats": "Amagar los estatistics de publicacion (ex. lo ombre de favorits)", + "hide_post_stats": "Amagar las estatisticas de publicacion (ex. lo nombre de favorits)", "hide_user_stats": "Amagar las estatisticas de l’utilizaire (ex. lo nombre de seguidors)", "hide_filtered_statuses": "Amagar los estatuts filtrats", "import_followers_from_a_csv_file": "Importar los seguidors d’un fichièr csv", "import_theme": "Cargar un tèma", "inputRadius": "Camps tèxte", "checkboxRadius": "Casas de marcar", - "instance_default": "(defaut : {value})", + "instance_default": "(defaut : {value})", "instance_default_simple": "(defaut)", "interface": "Interfàcia", "interfaceLanguage": "Lenga de l’interfàcia", @@ -172,7 +174,7 @@ "loop_video": "Bocla vidèo", "loop_video_silent_only": "Legir en bocla solament las vidèos sens son (coma los « Gifs » de Mastodon)", "mutes_tab": "Agamats", - "play_videos_in_modal": "Legir las vidèoas dirèctament dins la visualizaira mèdia", + "play_videos_in_modal": "Legir las vidèos dirèctament dins la visualizaira mèdia", "use_contain_fit": "Talhar pas las pèças juntas per las vinhetas", "name": "Nom", "name_bio": "Nom & Bio", @@ -223,7 +225,7 @@ "post_status_content_type": "Publicar lo tipe de contengut dels estatuts", "stop_gifs": "Lançar los GIFs al subrevòl", "streaming": "Activar lo cargament automatic dels novèls estatus en anar amont", - "text": "Tèxt", + "text": "Tèxte", "theme": "Tèma", "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", @@ -234,6 +236,117 @@ "values": { "false": "non", "true": "òc" + }, + "notifications": "Notificacions", + "enable_web_push_notifications": "Activar las notificacions web push", + "style": { + "switcher": { + "keep_color": "Gardar las colors", + "keep_shadows": "Gardar las ombras", + "keep_opacity": "Gardar l’opacitat", + "keep_roundness": "Gardar la redondetat", + "keep_fonts": "Gardar las polissas", + "save_load_hint": "Las opcions « Gardar » permeton de servar las opcions configuradas actualament quand seleccionatz o cargatz un tèma, permeton tanben d’enregistrar aquelas opcions quand exportatz un tèma. Quand totas las casas son pas marcadas, l’exportacion de tèma o enregistrarà tot.", + "reset": "Restablir", + "clear_all": "O escafar tot", + "clear_opacity": "Escafar l’opacitat" + }, + "common": { + "color": "Color", + "opacity": "Opacitat", + "contrast": { + "hint": "Lo coeficient de contraste es de {ratio}. Dòna {level} {context}", + "level": { + "aa": "un nivèl AA minimum recomandat", + "aaa": "un nivèl AAA recomandat", + "bad": "pas un nivèl d’accessibilitat recomandat" + }, + "context": { + "18pt": "pel tèxte grand (18pt+)", + "text": "pel tèxte" + } + } + }, + "common_colors": { + "_tab_label": "Comun", + "main": "Colors comunas", + "foreground_hint": "Vejatz « Avançat » per mai de paramètres detalhats", + "rgbo": "Icònas, accents, badges" + }, + "advanced_colors": { + "_tab_label": "Avançat", + "alert": "Rèire plan d’alèrtas", + "alert_error": "Error", + "badge": "Rèire plan dels badges", + "badge_notification": "Notificacion", + "panel_header": "Bandièra del tablèu de bòrd", + "top_bar": "Barra amont", + "borders": "Caires", + "buttons": "Botons", + "inputs": "Camps tèxte", + "faint_text": "Tèxte descolorit" + }, + "radii": { + "_tab_label": "Redondetat" + }, + "shadows": { + "_tab_label": "Ombra e luminositat", + "component": "Compausant", + "override": "Subrecargar", + "shadow_id": "Ombra #{value}", + "blur": "Fosc", + "spread": "Espandiment", + "inset": "Incrustacion", + "hint": "Per las ombras podètz tanben utilizar --variable coma valor de color per emplegar una variable CSS3. Notatz que lo paramètre d’opacitat foncionarà pas dins aquel cas.", + "filter_hint": { + "always_drop_shadow": "Avertiment, aquel ombra utiliza totjorn {0} quand lo navigator es compatible.", + "drop_shadow_syntax": "{0} es pas compatible amb lo paramètre {1} e lo mot clau {2}.", + "avatar_inset": "Notatz que combinar d’ombras incrustadas e pas incrustadas pòt donar de resultats inesperats amb los avatars transparents.", + "spread_zero": "L’ombra amb un espandiment de > 0 apareisserà coma reglat a zèro", + "inset_classic": "L’ombra d’incrustacion utilizarà {0}" + }, + "components": { + "panel": "Tablèu", + "panelHeader": "Bandièra del tablèu", + "topBar": "Barra amont", + "avatar": "Utilizar l’avatar (vista perfil)", + "avatarStatus": "Avatar de l’utilizaire (afichatge publicacion)", + "popup": "Fenèstras sorgissentas e astúcias", + "button": "Boton", + "buttonHover": "Boton (en passar la mirga)", + "buttonPressed": "Boton (en quichar)", + "buttonPressedHover": "Boton (en quichar e passar)", + "input": "Camp tèxte" + } + }, + "fonts": { + "_tab_label": "Polissas", + "help": "Selecionatz la polissa d’utilizar pels elements de l’UI. Per « Personalizada » vos cal picar lo nom exacte tal coma apareis sul sistèma.", + "components": { + "interface": "Interfàcia", + "input": "Camps tèxte", + "post": "Tèxte de publicacion", + "postCode": "Tèxte Monospaced dins las publicacion (tèxte formatat)" + }, + "family": "Nom de la polissa", + "size": "Talha (en px)", + "weight": "Largor (gras)", + "custom": "Personalizada" + }, + "preview": { + "header": "Apercebut", + "content": "Contengut", + "error": "Error d’exemple", + "button": "Boton", + "text": "A tròç de mai de {0} e {1}", + "mono": "contengut", + "input": "arribada al país.", + "faint_link": "manual d’ajuda", + "fine_print": "Legissètz nòstre {0} per legir pas res d’util !", + "header_faint": "Va plan", + "checkbox": "Ai legit los tèrmes e condicions d’utilizacion", + "link": "un pichon ligam simpatic" + } } }, "timeline": { @@ -241,19 +354,21 @@ "conversation": "Conversacion", "error_fetching": "Error en cercant de mesas a jorn", "load_older": "Ne veire mai", + "no_retweet_hint": "Las publicacions marcadas pels seguidors solament o dirèctas se pòdon pas repetir", "repeated": "repetit", "show_new": "Ne veire mai", "up_to_date": "A jorn", - "no_retweet_hint": "La publicacion marcada coma pels seguidors solament o dirècte pòt pas èsser repetida" + "no_more_statuses": "Pas mai d’estatuts", + "no_statuses": "Cap d’estatuts" }, "status": { - "reply_to": "Respondre à", + "reply_to": "Respond a", "replies_list": "Responsas :" }, "user_card": { "approve": "Validar", "block": "Blocar", - "blocked": "Blocat !", + "blocked": "Blocat !", "deny": "Refusar", "favorites": "Favorits", "follow": "Seguir", @@ -263,8 +378,8 @@ "follow_unfollow": "Quitar de seguir", "followees": "Abonaments", "followers": "Seguidors", - "following": "Seguit !", - "follows_you": "Vos sèc !", + "following": "Seguit !", + "follows_you": "Vos sèc !", "its_you": "Sètz vos !", "media": "Mèdia", "mute": "Amagar", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 29ab995b..41a34483 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -51,7 +51,7 @@ "public_tl": "Linha do tempo pública", "timeline": "Linha do tempo", "twkn": "Toda a rede conhecida", - "user_search": "Busca de usuário", + "user_search": "Buscar usuários", "who_to_follow": "Quem seguir", "preferences": "Preferências" }, @@ -67,8 +67,8 @@ }, "post_status": { "new_status": "Postar novo status", - "account_not_locked_warning": "Sua conta não está {0}. Qualquer pessoa pode te seguir para ver seus posts restritos.", - "account_not_locked_warning_link": "fechada", + "account_not_locked_warning": "Sua conta não é {0}. Qualquer pessoa pode te seguir e ver seus posts privados (só para seguidores).", + "account_not_locked_warning_link": "restrita", "attachments_sensitive": "Marcar anexos como sensíveis", "content_type": { "text/plain": "Texto puro" @@ -115,7 +115,7 @@ "avatarRadius": "Avatares", "background": "Pano de Fundo", "bio": "Biografia", - "blocks_tab": "Blocos", + "blocks_tab": "Bloqueios", "btnRadius": "Botões", "cBlue": "Azul (Responder, seguir)", "cGreen": "Verde (Repetir)", @@ -125,7 +125,7 @@ "change_password_error": "Houve um erro ao modificar sua senha.", "changed_password": "Senha modificada com sucesso!", "collapse_subject": "Esconder posts com assunto", - "composing": "Escrevendo", + "composing": "Escrita", "confirm_new_password": "Confirmar nova senha", "current_avatar": "Seu avatar atual", "current_password": "Sua senha atual", @@ -139,7 +139,7 @@ "avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.", "export_theme": "Salvar predefinições", "filtering": "Filtragem", - "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas, uma por linha.", + "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas; uma palavra por linha.", "follow_export": "Exportar quem você segue", "follow_export_button": "Exportar quem você segue para um arquivo CSV", "follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo", @@ -178,7 +178,7 @@ "name_bio": "Nome & Biografia", "new_password": "Nova senha", "notification_visibility": "Tipos de notificação para mostrar", - "notification_visibility_follows": "Seguidos", + "notification_visibility_follows": "Seguidas", "notification_visibility_likes": "Favoritos", "notification_visibility_mentions": "Menções", "notification_visibility_repeats": "Repetições", @@ -187,7 +187,7 @@ "no_mutes": "Sem silenciados", "hide_follows_description": "Não mostrar quem estou seguindo", "hide_followers_description": "Não mostrar quem me segue", - "show_admin_badge": "Mostrar distintivo de Administrador em meu perfil", + "show_admin_badge": "Mostrar título de Administrador em meu perfil", "show_moderator_badge": "Mostrar título de Moderador em meu perfil", "nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis", "oauth_tokens": "Token OAuth", @@ -201,9 +201,9 @@ "profile_background": "Pano de fundo de perfil", "profile_banner": "Capa de perfil", "profile_tab": "Perfil", - "radii_help": "Arredondar arestas da interface (em píxeis)", + "radii_help": "Arredondar arestas da interface (em pixel)", "replies_in_timeline": "Respostas na linha do tempo", - "reply_link_preview": "Habilitar a pré-visualização de link de respostas ao passar o mouse.", + "reply_link_preview": "Habilitar a pré-visualização de de respostas ao passar o mouse.", "reply_visibility_all": "Mostrar todas as respostas", "reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo", "reply_visibility_self": "Só mostrar respostas direcionadas a mim", @@ -212,7 +212,7 @@ "security_tab": "Segurança", "scope_copy": "Copiar opções de privacidade ao responder (Mensagens diretas sempre copiam)", "set_new_avatar": "Alterar avatar", - "set_new_profile_background": "Alterar o plano de fundo de perfil", + "set_new_profile_background": "Alterar o pano de fundo de perfil", "set_new_profile_banner": "Alterar capa de perfil", "settings": "Configurações", "subject_input_always_show": "Sempre mostrar campo de assunto", @@ -220,9 +220,9 @@ "subject_line_email": "Como em email: \"re: assunto\"", "subject_line_mastodon": "Como o Mastodon: copiar como está", "subject_line_noop": "Não copiar", - "post_status_content_type": "Postar tipo de conteúdo do status", - "stop_gifs": "Reproduzir GIFs ao passar o cursor em cima", - "streaming": "Habilitar o fluxo automático de postagens quando ao topo da página", + "post_status_content_type": "Tipo de conteúdo do status", + "stop_gifs": "Reproduzir GIFs ao passar o cursor", + "streaming": "Habilitar o fluxo automático de postagens no topo da página", "text": "Texto", "theme": "Tema", "theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.", @@ -235,7 +235,7 @@ "false": "não", "true": "sim" }, - "notifications": "Notifications", + "notifications": "Notificações", "enable_web_push_notifications": "Habilitar notificações web push", "style": { "switcher": { @@ -245,7 +245,7 @@ "keep_roundness": "Manter arredondado", "keep_fonts": "Manter fontes", "save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.", - "reset": "Voltar ao padrão", + "reset": "Restaurar o padrão", "clear_all": "Limpar tudo", "clear_opacity": "Limpar opacidade" }, @@ -319,7 +319,7 @@ }, "fonts": { "_tab_label": "Fontes", - "help": "Selecionar fonte dos elementos da interface. Para fonte \"personalizada\" você deve entrar exatamente o nome da fonte no sistema.", + "help": "Selecione as fontes dos elementos da interface. Para fonte \"personalizada\" você deve inserir o mesmo nome da fonte no sistema.", "components": { "interface": "Interface", "input": "Campo de entrada", @@ -383,7 +383,7 @@ "mute": "Silenciar", "muted": "Silenciado", "per_day": "por dia", - "remote_follow": "Seguidor Remoto", + "remote_follow": "Seguir remotamente", "statuses": "Postagens", "unblock": "Desbloquear", "unblock_progress": "Desbloqueando...", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 7ab89c12..720ff706 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -60,6 +60,9 @@ export default function createPersistedState ({ merge({}, store.state, savedState) ) } + if (store.state.oauth.token) { + store.dispatch('loginUser', store.state.oauth.token) + } loaded = true } catch (e) { console.log("Couldn't load state") diff --git a/src/modules/config.js b/src/modules/config.js index 1c30c203..c5491c01 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -5,6 +5,7 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0] const defaultState = { colors: {}, + hideMutedPosts: undefined, // instance default collapseMessageWithSubject: undefined, // instance default hideAttachments: false, hideAttachmentsInConv: false, diff --git a/src/modules/instance.js b/src/modules/instance.js index 24c52f9c..f778ac4d 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -18,6 +18,7 @@ const defaultState = { scopeOptionsEnabled: true, formattingOptionsEnabled: false, alwaysShowSubjectInput: true, + hideMutedPosts: false, collapseMessageWithSubject: false, hidePostStats: false, hideUserStats: false, @@ -48,7 +49,11 @@ const defaultState = { // Html stuff instanceSpecificPanelContent: '', - tos: '' + tos: '', + + // Version Information + backendVersion: '', + frontendVersion: '' } const instance = { diff --git a/src/modules/statuses.js b/src/modules/statuses.js index f14b8703..a16342e0 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -1,4 +1,5 @@ import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash' +import { set } from 'vue' import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' @@ -82,7 +83,7 @@ const mergeOrAdd = (arr, obj, item) => { // This is a new item, prepare it prepareStatus(item) arr.push(item) - obj[item.id] = item + set(obj, item.id, item) return {item, new: true} } } diff --git a/src/modules/users.js b/src/modules/users.js index 1fe12fc8..5cfa128e 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -16,9 +16,9 @@ export const mergeOrAdd = (arr, obj, item) => { } else { // This is a new item, prepare it arr.push(item) - obj[item.id] = item + set(obj, item.id, item) if (item.screen_name && !item.screen_name.includes('@')) { - obj[item.screen_name.toLowerCase()] = item + set(obj, item.screen_name.toLowerCase(), item) } return { item, new: true } } @@ -102,10 +102,20 @@ export const mutations = { } }) }, - saveBlocks (state, blockIds) { + updateBlocks (state, blockedUsers) { + // Reset statusnet_blocking of all fetched users + each(state.users, (user) => { user.statusnet_blocking = false }) + each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user)) + }, + saveBlockIds (state, blockIds) { state.currentUser.blockIds = blockIds }, - saveMutes (state, muteIds) { + updateMutes (state, mutedUsers) { + // Reset muted of all fetched users + each(state.users, (user) => { user.muted = false }) + each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user)) + }, + saveMuteIds (state, muteIds) { state.currentUser.muteIds = muteIds }, setUserForStatus (state, status) { @@ -172,34 +182,39 @@ const users = { fetchBlocks (store) { return store.rootState.api.backendInteractor.fetchBlocks() .then((blocks) => { - store.commit('saveBlocks', map(blocks, 'id')) - store.commit('addNewUsers', blocks) + store.commit('saveBlockIds', map(blocks, 'id')) + store.commit('updateBlocks', blocks) return blocks }) }, - blockUser (store, id) { - return store.rootState.api.backendInteractor.blockUser(id) - .then((user) => store.commit('addNewUsers', [user])) + blockUser (store, userId) { + return store.rootState.api.backendInteractor.blockUser(userId) + .then((relationship) => { + store.commit('updateUserRelationship', [relationship]) + store.commit('removeStatus', { timeline: 'friends', userId }) + store.commit('removeStatus', { timeline: 'public', userId }) + store.commit('removeStatus', { timeline: 'publicAndExternal', userId }) + }) }, unblockUser (store, id) { return store.rootState.api.backendInteractor.unblockUser(id) - .then((user) => store.commit('addNewUsers', [user])) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) }, fetchMutes (store) { return store.rootState.api.backendInteractor.fetchMutes() - .then((mutedUsers) => { - each(mutedUsers, (user) => { user.muted = true }) - store.commit('addNewUsers', mutedUsers) - store.commit('saveMutes', map(mutedUsers, 'id')) + .then((mutes) => { + store.commit('updateMutes', mutes) + store.commit('saveMuteIds', map(mutes, 'id')) + return mutes }) }, muteUser (store, id) { - return store.state.api.backendInteractor.setUserMute({ id, muted: true }) - .then((user) => store.commit('addNewUsers', [user])) + return store.rootState.api.backendInteractor.muteUser(id) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) }, unmuteUser (store, id) { - return store.state.api.backendInteractor.setUserMute({ id, muted: false }) - .then((user) => store.commit('addNewUsers', [user])) + return store.rootState.api.backendInteractor.unmuteUser(id) + .then((relationship) => store.commit('updateUserRelationship', [relationship])) }, addFriends ({ rootState, commit }, fetchBy) { return new Promise((resolve, reject) => { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index fab48266..2f54d508 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -9,19 +9,13 @@ const FAVORITE_URL = '/api/favorites/create' const UNFAVORITE_URL = '/api/favorites/destroy' const RETWEET_URL = '/api/statuses/retweet' const UNRETWEET_URL = '/api/statuses/unretweet' -const STATUS_UPDATE_URL = '/api/statuses/update.json' const STATUS_DELETE_URL = '/api/statuses/destroy' -const STATUS_URL = '/api/statuses/show' -const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' -const CONVERSATION_URL = '/api/statusnet/conversation' const MENTIONS_URL = '/api/statuses/mentions.json' const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json' const FOLLOWERS_URL = '/api/statuses/followers.json' const FRIENDS_URL = '/api/statuses/friends.json' -const BLOCKS_URL = '/api/statuses/blocks.json' const FOLLOWING_URL = '/api/friendships/create.json' const UNFOLLOWING_URL = '/api/friendships/destroy.json' -const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json' const REGISTRATION_URL = '/api/account/register.json' const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json' const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' @@ -29,8 +23,6 @@ const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json' const PROFILE_UPDATE_URL = '/api/account/update_profile.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' -const BLOCKING_URL = '/api/blocks/create.json' -const UNBLOCKING_URL = '/api/blocks/destroy.json' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' @@ -41,12 +33,22 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' +const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` +const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` +const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/' +const MASTODON_USER_MUTES_URL = '/api/v1/mutes/' +const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block` +const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock` +const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute` +const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` +const MASTODON_POST_STATUS_URL = '/api/v1/statuses' +const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' import { each, map } from 'lodash' -import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' import 'whatwg-fetch' import { StatusCodeError } from '../errors/errors' @@ -60,6 +62,19 @@ let fetch = (url, options) => { return oldfetch(fullUrl, options) } +const promisedRequest = (url, options) => { + return fetch(url, options) + .then((response) => { + return new Promise((resolve, reject) => response.json() + .then((json) => { + if (!response.ok) { + return reject(new StatusCodeError(response.status, json, { url, options }, response)) + } + return resolve(json) + })) + }) +} + // Params // cropH // cropW @@ -212,16 +227,14 @@ const unfollowUser = ({id, credentials}) => { } const blockUser = ({id, credentials}) => { - let url = `${BLOCKING_URL}?user_id=${id}` - return fetch(url, { + return fetch(MASTODON_BLOCK_USER_URL(id), { headers: authHeaders(credentials), method: 'POST' }).then((data) => data.json()) } const unblockUser = ({id, credentials}) => { - let url = `${UNBLOCKING_URL}?user_id=${id}` - return fetch(url, { + return fetch(MASTODON_UNBLOCK_USER_URL(id), { headers: authHeaders(credentials), method: 'POST' }).then((data) => data.json()) @@ -245,16 +258,7 @@ const denyUser = ({id, credentials}) => { const fetchUser = ({id, credentials}) => { let url = `${MASTODON_USER_URL}/${id}` - return fetch(url, { headers: authHeaders(credentials) }) - .then((response) => { - return new Promise((resolve, reject) => response.json() - .then((json) => { - if (!response.ok) { - return reject(new StatusCodeError(response.status, json, { url }, response)) - } - return resolve(json) - })) - }) + return promisedRequest(url, { headers: authHeaders(credentials) }) .then((data) => parseUser(data)) } @@ -313,8 +317,8 @@ const fetchFollowRequests = ({credentials}) => { } const fetchConversation = ({id, credentials}) => { - let url = `${CONVERSATION_URL}/${id}.json?count=100` - return fetch(url, { headers: authHeaders(credentials) }) + let urlContext = MASTODON_STATUS_CONTEXT_URL(id) + return fetch(urlContext, { headers: authHeaders(credentials) }) .then((data) => { if (data.ok) { return data @@ -322,11 +326,14 @@ const fetchConversation = ({id, credentials}) => { throw new Error('Error fetching timeline', data) }) .then((data) => data.json()) - .then((data) => data.map(parseStatus)) + .then(({ancestors, descendants}) => ({ + ancestors: ancestors.map(parseStatus), + descendants: descendants.map(parseStatus) + })) } const fetchStatus = ({id, credentials}) => { - let url = `${STATUS_URL}/${id}.json` + let url = MASTODON_STATUS_URL(id) return fetch(url, { headers: authHeaders(credentials) }) .then((data) => { if (data.ok) { @@ -338,23 +345,7 @@ const fetchStatus = ({id, credentials}) => { .then((data) => parseStatus(data)) } -const setUserMute = ({id, credentials, muted = true}) => { - const form = new FormData() - - const muteInteger = muted ? 1 : 0 - - form.append('namespace', 'qvitter') - form.append('data', muteInteger) - form.append('topic', `mute:${id}`) - - return fetch(QVITTER_USER_PREF_URL, { - method: 'POST', - headers: authHeaders(credentials), - body: form - }) -} - -const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => { +const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, friends: FRIENDS_TIMELINE_URL, @@ -390,6 +381,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use } params.push(['count', 20]) + params.push(['with_muted', withMuted]) const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') url += `?${queryString}` @@ -450,23 +442,23 @@ const unretweet = ({ id, credentials }) => { }) } -const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks}) => { - const idsText = mediaIds.join(',') +const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => { const form = new FormData() form.append('status', status) form.append('source', 'Pleroma FE') - if (noAttachmentLinks) form.append('no_attachment_links', noAttachmentLinks) if (spoilerText) form.append('spoiler_text', spoilerText) if (visibility) form.append('visibility', visibility) if (sensitive) form.append('sensitive', sensitive) if (contentType) form.append('content_type', contentType) - form.append('media_ids', idsText) + mediaIds.forEach(val => { + form.append('media_ids[]', val) + }) if (inReplyToStatusId) { - form.append('in_reply_to_status_id', inReplyToStatusId) + form.append('in_reply_to_id', inReplyToStatusId) } - return fetch(STATUS_UPDATE_URL, { + return fetch(MASTODON_POST_STATUS_URL, { body: form, method: 'POST', headers: authHeaders(credentials) @@ -491,13 +483,13 @@ const deleteStatus = ({ id, credentials }) => { } const uploadMedia = ({formData, credentials}) => { - return fetch(MEDIA_UPLOAD_URL, { + return fetch(MASTODON_MEDIA_UPLOAD_URL, { body: formData, method: 'POST', headers: authHeaders(credentials) }) - .then((response) => response.text()) - .then((text) => (new DOMParser()).parseFromString(text, 'application/xml')) + .then((data) => data.json()) + .then((data) => parseAttachment(data)) } const followImport = ({params, credentials}) => { @@ -538,30 +530,40 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma } const fetchMutes = ({credentials}) => { - const url = '/api/qvitter/mutes.json' - - return fetch(url, { - headers: authHeaders(credentials) - }).then((data) => data.json()) + return promisedRequest(MASTODON_USER_MUTES_URL, { headers: authHeaders(credentials) }) + .then((users) => users.map(parseUser)) } -const fetchBlocks = ({page, credentials}) => { - return fetch(BLOCKS_URL, { - headers: authHeaders(credentials) - }).then((data) => { - if (data.ok) { - return data.json() - } - throw new Error('Error fetching blocks', data) +const muteUser = ({id, credentials}) => { + return promisedRequest(MASTODON_MUTE_USER_URL(id), { + headers: authHeaders(credentials), + method: 'POST' }) } +const unmuteUser = ({id, credentials}) => { + return promisedRequest(MASTODON_UNMUTE_USER_URL(id), { + headers: authHeaders(credentials), + method: 'POST' + }) +} + +const fetchBlocks = ({credentials}) => { + return promisedRequest(MASTODON_USER_BLOCKS_URL, { headers: authHeaders(credentials) }) + .then((users) => users.map(parseUser)) +} + const fetchOAuthTokens = ({credentials}) => { const url = '/api/oauth_tokens.json' return fetch(url, { headers: authHeaders(credentials) - }).then((data) => data.json()) + }).then((data) => { + if (data.ok) { + return data.json() + } + throw new Error('Error fetching auth tokens', data) + }) } const revokeOAuthToken = ({id, credentials}) => { @@ -613,8 +615,9 @@ const apiService = { deleteStatus, uploadMedia, fetchAllFollowing, - setUserMute, fetchMutes, + muteUser, + unmuteUser, fetchBlocks, fetchOAuthTokens, revokeOAuthToken, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index cbd0b733..0f0bcddc 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -62,12 +62,10 @@ const backendInteractorService = (credentials) => { return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag}) } - const setUserMute = ({id, muted = true}) => { - return apiService.setUserMute({id, muted, credentials}) - } - const fetchMutes = () => apiService.fetchMutes({credentials}) - const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params}) + const muteUser = (id) => apiService.muteUser({credentials, id}) + const unmuteUser = (id) => apiService.unmuteUser({credentials, id}) + const fetchBlocks = () => apiService.fetchBlocks({credentials}) const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials}) const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials}) const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials}) @@ -100,8 +98,9 @@ const backendInteractorService = (credentials) => { fetchAllFollowing, verifyCredentials: apiService.verifyCredentials, startFetching, - setUserMute, fetchMutes, + muteUser, + unmuteUser, fetchBlocks, fetchOAuthTokens, revokeOAuthToken, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 3bd42b94..249e99b4 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -128,14 +128,15 @@ export const parseUser = (data) => { return output } -const parseAttachment = (data) => { +export const parseAttachment = (data) => { const output = {} const masto = !data.hasOwnProperty('oembed') if (masto) { // Not exactly same... - output.mimetype = data.type + output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type output.meta = data.meta // not present in BE yet + output.id = data.id } else { output.mimetype = data.mimetype // output.meta = ??? missing @@ -311,5 +312,5 @@ export const parseNotification = (data) => { const isNsfw = (status) => { const nsfwRegex = /#nsfw/i - return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) + return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex) } diff --git a/src/services/new_api/user_search.js b/src/services/new_api/user_search.js index ce7da88e..869afa9c 100644 --- a/src/services/new_api/user_search.js +++ b/src/services/new_api/user_search.js @@ -1,13 +1,16 @@ import utils from './utils.js' +import { parseUser } from '../entity_normalizer/entity_normalizer.service.js' const search = ({query, store}) => { return utils.request({ store, - url: '/api/pleroma/search_user', + url: '/api/v1/accounts/search', params: { - query + q: query } - }).then((data) => data.json()) + }) + .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const UserSearch = { search diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index f1932bb6..e70b0f26 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -4,7 +4,7 @@ import apiService from '../api/api.service.js' const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => { const mediaIds = map(media, 'id') - return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks}) + return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) .then((data) => { if (!data.error) { store.dispatch('addNewStatuses', { @@ -26,25 +26,7 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = const uploadMedia = ({ store, formData }) => { const credentials = store.state.users.currentUser.credentials - return apiService.uploadMedia({ credentials, formData }).then((xml) => { - // Firefox and Chrome treat method differently... - let link = xml.getElementsByTagName('link') - - if (link.length === 0) { - link = xml.getElementsByTagName('atom:link') - } - - link = link[0] - - const mediaData = { - id: xml.getElementsByTagName('media_id')[0].textContent, - url: xml.getElementsByTagName('media_url')[0].textContent, - image: link.getAttribute('href'), - mimetype: link.getAttribute('type') - } - - return mediaData - }) + return apiService.uploadMedia({ credentials, formData }) } const statusPosterService = { diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 6f99616f..8e954cdf 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -19,6 +19,9 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false const args = { timeline, credentials } const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] + const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined' + ? rootState.instance.hideMutedPosts + : rootState.config.hideMutedPosts if (older) { args['until'] = until || timelineData.minId @@ -28,6 +31,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false args['userId'] = userId args['tag'] = tag + args['withMuted'] = !hideMutedPosts const numStatusesBeforeFetch = timelineData.statuses.length diff --git a/src/services/version/version.service.js b/src/services/version/version.service.js new file mode 100644 index 00000000..a750b0dd --- /dev/null +++ b/src/services/version/version.service.js @@ -0,0 +1,6 @@ + +export const extractCommit = versionString => { + const regex = /-g(\w+)$/i + const matches = versionString.match(regex) + return matches ? matches[1] : '' +}