diff options
| -rw-r--r-- | src/boot/after_store.js | 3 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.js | 21 | ||||
| -rw-r--r-- | src/components/user_card_content/user_card_content.vue | 145 | ||||
| -rw-r--r-- | src/components/who_to_follow_panel/who_to_follow_panel.js | 98 | ||||
| -rw-r--r-- | src/components/who_to_follow_panel/who_to_follow_panel.vue | 9 | ||||
| -rw-r--r-- | src/i18n/ja.json | 171 | ||||
| -rw-r--r-- | src/lib/push_notifications_plugin.js | 22 | ||||
| -rw-r--r-- | src/main.js | 28 | ||||
| -rw-r--r-- | src/modules/config.js | 2 | ||||
| -rw-r--r-- | src/modules/instance.js | 1 | ||||
| -rw-r--r-- | src/modules/statuses.js | 2 | ||||
| -rw-r--r-- | src/modules/users.js | 28 | ||||
| -rw-r--r-- | src/services/api/api.service.js | 3 | ||||
| -rw-r--r-- | src/services/push/push.js | 81 | ||||
| -rw-r--r-- | src/services/status_poster/status_poster.service.js | 2 | ||||
| -rw-r--r-- | static/config.json | 4 |
16 files changed, 432 insertions, 188 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 5b9e5c96..6e87d5f6 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -72,6 +72,7 @@ const afterStoreSetup = ({ store, i18n }) => { var scopeCopy = (config.scopeCopy) var subjectLineBehavior = (config.subjectLineBehavior) var alwaysShowSubjectInput = (config.alwaysShowSubjectInput) + var noAttachmentLinks = (config.noAttachmentLinks) store.dispatch('setInstanceOption', { name: 'theme', value: theme }) store.dispatch('setInstanceOption', { name: 'background', value: background }) @@ -90,6 +91,8 @@ const afterStoreSetup = ({ store, i18n }) => { store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy }) store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior }) store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput }) + store.dispatch('setInstanceOption', { name: 'noAttachmentLinks', value: noAttachmentLinks }) + if (chatDisabled) { store.dispatch('disableChat') } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 89091f8e..8a4e2489 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -32,6 +32,8 @@ const PostStatusForm = { }, mounted () { this.resize(this.$refs.textarea) + const textLength = this.$refs.textarea.value.length + this.$refs.textarea.setSelectionRange(textLength, textLength) if (this.replyTo) { this.$refs.textarea.focus() @@ -250,7 +252,8 @@ const PostStatusForm = { } this.$emit('posted') let el = this.$el.querySelector('textarea') - el.style.height = '16px' + el.style.height = 'auto' + el.style.height = undefined this.error = null } else { this.error = data.error @@ -298,13 +301,15 @@ const PostStatusForm = { e.dataTransfer.dropEffect = 'copy' }, resize (e) { - if (!e.target) { return } - const vertPadding = Number(window.getComputedStyle(e.target)['padding-top'].substr(0, 1)) + - Number(window.getComputedStyle(e.target)['padding-bottom'].substr(0, 1)) - e.target.style.height = 'auto' - e.target.style.height = `${e.target.scrollHeight - vertPadding}px` - if (e.target.value === '') { - e.target.style.height = '16px' + const target = e.target || e + if (!(target instanceof window.Element)) { return } + const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) + + Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1)) + // Auto is needed to make textbox shrink when removing lines + target.style.height = 'auto' + target.style.height = `${target.scrollHeight - vertPadding}px` + if (target.value === '') { + target.style.height = null } }, clearError () { diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 1f5c9ab6..3804302b 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -2,22 +2,25 @@ <div id="heading" class="profile-panel-background" :style="headingStyle"> <div class="panel-heading text-center"> <div class='user-info'> - <router-link :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" v-if="!isOtherUser"> - <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> - </router-link> - <a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser"> - <i class="icon-link-ext usersettings"></i> - </a> <div class='container'> <router-link :to="userProfileLink(user)"> <StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/> </router-link> <div class="name-and-screen-name"> - <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div> - <div :title="user.name" class='user-name' v-else>{{user.name}}</div> + <div class="top-line"> + <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div> + <div :title="user.name" class='user-name' v-else>{{user.name}}</div> + <router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser"> + <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> + </router-link> + <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser"> + <i class="icon-link-ext usersettings"></i> + </a> + </div> + <router-link class='user-screen-name' :to="userProfileLink(user)"> - <span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span> - <span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span> + <span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span> + <span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span> </router-link> </div> </div> @@ -25,7 +28,7 @@ <div v-if="user.follows_you && loggedIn && isOtherUser" class="following"> {{ $t('user_card.follows_you') }} </div> - <div class="floater" v-if="isOtherUser && (loggedIn || !switcher)"> + <div class="highlighter" v-if="isOtherUser && (loggedIn || !switcher)"> <!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to --> <input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/> <input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/> @@ -139,7 +142,7 @@ border-bottom-right-radius: 0; .panel-heading { - padding: 0.6em 0em; + padding: .6em 0; text-align: center; box-shadow: none; } @@ -158,10 +161,10 @@ .user-info { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); - padding: 0 16px; + padding: 0 26px; .container { - padding: 16px 10px 6px 10px; + padding: 16px 0 6px; display: flex; max-height: 56px; @@ -218,11 +221,15 @@ vertical-align: middle; object-fit: contain } + .top-line { + display: flex; + } } .user-name{ text-overflow: ellipsis; overflow: hidden; + flex: 1 0 auto; } .user-screen-name { @@ -232,27 +239,73 @@ font-weight: light; font-size: 15px; padding-right: 0.1em; + width: 100%; + display: flex; + + .dailyAvg { + min-width: 1px; + flex: 0 0 auto; + } + + .handle { + min-width: 1px; + flex: 0 1 auto; + text-overflow: ellipsis; + overflow: hidden; + } } .user-meta { - margin-bottom: .4em; + margin-bottom: .15em; + display: flex; + align-items: baseline; + font-size: 14px; + line-height: 22px; + flex-wrap: wrap; .following { - font-size: 14px; - flex: 0 0 100%; + flex: 1 0 auto; margin: 0; - padding-left: 16px; + margin-bottom: .25em; text-align: left; - float: left; - } - .floater { - margin: 0; } - &::after { - display: block; - content: ''; - clear: both; + .highlighter { + flex: 0 1 auto; + display: flex; + flex-wrap: wrap; + margin-right: -.5em; + align-self: start; + + .userHighlightCl { + padding: 2px 10px; + flex: 1 0 auto; + } + + .userHighlightSel, + .userHighlightSel.select { + padding-top: 0; + padding-bottom: 0; + flex: 1 0 auto; + } + .userHighlightSel.select i { + line-height: 22px; + } + + .userHighlightText { + width: 70px; + flex: 1 0 auto; + } + + .userHighlightCl, + .userHighlightText, + .userHighlightSel, + .userHighlightSel.select { + height: 22px; + vertical-align: top; + margin-right: .5em; + margin-bottom: .25em; + } } } .user-interactions { @@ -260,8 +313,13 @@ flex-flow: row wrap; justify-content: space-between; + margin-right: -.75em; + div { - flex: 1; + flex: 1 0 0; + margin-right: .75em; + margin-bottom: .6em; + white-space: nowrap; } .mute { @@ -280,8 +338,9 @@ } button { - width: 92%; + width: 100%; height: 100%; + margin: 0; } .remote-button { @@ -304,10 +363,11 @@ justify-content: space-between; color: $fallback--lightText; color: var(--lightText, $fallback--lightText); + flex-wrap: wrap; } .user-count { - flex: 1; + flex: 1 0 auto; padding: .5em 0 .5em 0; margin: 0 .5em; @@ -327,32 +387,5 @@ color: #CCC; } .floater { - float: right; - margin-top: 16px; - - .userHighlightCl { - padding: 2px 10px; - } - .userHighlightSel, - .userHighlightSel.select { - padding-top: 0; - padding-bottom: 0; - } - .userHighlightSel.select i { - line-height: 22px; - } - - .userHighlightText { - width: 70px; - } - - .userHighlightCl, - .userHighlightText, - .userHighlightSel, - .userHighlightSel.select { - height: 22px; - vertical-align: top; - margin-right: 0 - } } </style> 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 c2df6899..b2183e6d 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 @@ -2,62 +2,31 @@ import apiService from '../../services/api/api.service.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' function showWhoToFollow (panel, reply) { - var users = reply - var cn - var index - var step = 7 - cn = Math.floor(Math.random() * step) - for (index = 0; index < 3; index++) { - var user - user = users[cn] - var img - if (user.avatar) { - img = user.avatar - } else { - img = '/images/avi.png' - } - var name = user.acct - if (index === 0) { - panel.img1 = img - panel.name1 = name - panel.$store.state.api.backendInteractor.externalProfile(name) - .then((externalUser) => { - if (!externalUser.error) { - panel.$store.commit('addNewUsers', [externalUser]) - panel.id1 = externalUser.id - } - }) - } else if (index === 1) { - panel.img2 = img - panel.name2 = name - panel.$store.state.api.backendInteractor.externalProfile(name) - .then((externalUser) => { - if (!externalUser.error) { - panel.$store.commit('addNewUsers', [externalUser]) - panel.id2 = externalUser.id - } - }) - } else if (index === 2) { - panel.img3 = img - panel.name3 = name - panel.$store.state.api.backendInteractor.externalProfile(name) - .then((externalUser) => { - if (!externalUser.error) { - panel.$store.commit('addNewUsers', [externalUser]) - panel.id3 = externalUser.id - } - }) - } - cn = (cn + step) % users.length - } + panel.usersToFollow.forEach((toFollow, index) => { + let randIndex = Math.floor(Math.random() * reply.length) + let user = reply[randIndex] + let img = user.avatar || '/images/avi.png' + let name = user.acct + + toFollow.img = img + toFollow.name = name + + panel.$store.state.api.backendInteractor.externalProfile(name) + .then((externalUser) => { + if (!externalUser.error) { + panel.$store.commit('addNewUsers', [externalUser]) + toFollow.id = externalUser.id + } + }) + }) } function getWhoToFollow (panel) { var credentials = panel.$store.state.users.currentUser.credentials if (credentials) { - panel.name1 = 'Loading...' - panel.name2 = 'Loading...' - panel.name3 = 'Loading...' + panel.usersToFollow.forEach(toFollow => { + toFollow.name = 'Loading...' + }) apiService.suggestions({credentials: credentials}) .then((reply) => { showWhoToFollow(panel, reply) @@ -67,27 +36,24 @@ function getWhoToFollow (panel) { const WhoToFollowPanel = { data: () => ({ - img1: '/images/avi.png', - name1: '', - id1: 0, - img2: '/images/avi.png', - name2: '', - id2: 0, - img3: '/images/avi.png', - name3: '', - id3: 0 + usersToFollow: new Array(3).fill().map(x => ( + { + img: '/images/avi.png', + name: '', + id: 0 + } + )) }), computed: { user: function () { return this.$store.state.users.currentUser.screen_name }, moreUrl: function () { - var host = window.location.hostname - var user = this.user - var suggestionsWeb = this.$store.state.instance.suggestionsWeb - var url - url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host)) - url = url.replace(/{{user}}/g, encodeURIComponent(user)) + const host = window.location.hostname + const user = this.user + const suggestionsWeb = this.$store.state.instance.suggestionsWeb + const url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host)) + .replace(/{{user}}/g, encodeURIComponent(user)) return url }, suggestionsEnabled () { diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.vue b/src/components/who_to_follow_panel/who_to_follow_panel.vue index a62e8360..ad6a028e 100644 --- a/src/components/who_to_follow_panel/who_to_follow_panel.vue +++ b/src/components/who_to_follow_panel/who_to_follow_panel.vue @@ -8,9 +8,12 @@ </div> <div class="panel-body who-to-follow"> <p> - <img v-bind:src="img1"/> <router-link :to="userProfileLink(id1, name1)">{{ name1 }}</router-link><br> - <img v-bind:src="img2"/> <router-link :to="userProfileLink(id2, name2)">{{ name2 }}</router-link><br> - <img v-bind:src="img3"/> <router-link :to="userProfileLink(id3, name3)">{{ name3 }}</router-link><br> + <span v-for="user in usersToFollow"> + <img v-bind:src="user.img" /> + <router-link v-bind:to="userProfileLink(user.id, user.name)"> + {{user.name}} + </router-link><br /> + </span> <img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a> </p> </div> diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 4da7ea30..31ca4a52 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -29,13 +29,16 @@ "username": "ユーザーめい" }, "nav": { + "back": "もどる", "chat": "ローカルチャット", "friend_requests": "フォローリクエスト", "mentions": "メンション", "dms": "ダイレクトメッセージ", "public_tl": "パブリックタイムライン", "timeline": "タイムライン", - "twkn": "つながっているすべてのネットワーク" + "twkn": "つながっているすべてのネットワーク", + "user_search": "ユーザーをさがす", + "preferences": "せってい" }, "notifications": { "broken_favorite": "ステータスがみつかりません。さがしています...", @@ -70,7 +73,17 @@ "fullname": "スクリーンネーム", "password_confirm": "パスワードのかくにん", "registration": "はじめる", - "token": "しょうたいトークン" + "token": "しょうたいトークン", + "captcha": "CAPTCHA", + "new_captcha": "もじがよめないときは、がぞうをクリックすると、あたらしいがぞうになります", + "validations": { + "username_required": "なにかかいてください", + "fullname_required": "なにかかいてください", + "email_required": "なにかかいてください", + "password_required": "なにかかいてください", + "password_confirmation_required": "なにかかいてください", + "password_confirmation_match": "パスワードがちがいます" + } }, "settings": { "attachmentRadius": "ファイル", @@ -90,6 +103,7 @@ "change_password_error": "パスワードをかえることが、できなかったかもしれません。", "changed_password": "パスワードが、かわりました!", "collapse_subject": "せつめいのあるとうこうをたたむ", + "composing": "とうこう", "confirm_new_password": "あたらしいパスワードのかくにん", "current_avatar": "いまのアバター", "current_password": "いまのパスワード", @@ -113,17 +127,22 @@ "general": "ぜんぱん", "hide_attachments_in_convo": "スレッドのファイルをかくす", "hide_attachments_in_tl": "タイムラインのファイルをかくす", + "hide_isp": "インスタンススペシフィックパネルをかくす", + "preload_images": "がぞうをさきよみする", "hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)", "hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)", "import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする", "import_theme": "ロード", "inputRadius": "インプットフィールド", + "checkboxRadius": "チェックボックス", "instance_default": "(デフォルト: {value})", + "instance_default_simple": "(デフォルト)", + "interface": "インターフェース", "interfaceLanguage": "インターフェースのことば", "invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマはへんこうされませんでした。", "limited_availability": "あなたのブラウザではできません", "links": "リンク", - "lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできます", + "lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる", "loop_video": "ビデオをくりかえす", "loop_video_silent_only": "おとのないビデオだけくりかえす", "name": "なまえ", @@ -135,6 +154,7 @@ "notification_visibility_mentions": "メンション", "notification_visibility_repeats": "リピート", "no_rich_text_description": "リッチテキストをつかわない", + "hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない", "nsfw_clickthrough": "NSFWなファイルをかくす", "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", @@ -151,20 +171,139 @@ "saving_err": "せっていをセーブできませんでした", "saving_ok": "せっていをセーブしました", "security_tab": "セキュリティ", + "scope_copy": "リプライするとき、こうかいはんいをコピーする (DMのこうかいはんいは、つねにコピーされます)", "set_new_avatar": "あたらしいアバターをせっていする", "set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする", "set_new_profile_banner": "あたらしいプロフィールバナーを設定する", "settings": "せってい", + "subject_input_always_show": "サブジェクトフィールドをいつでもひょうじする", + "subject_line_behavior": "リプライするときサブジェクトをコピーする", + "subject_line_email": "メールふう: \"re: サブジェクト\"", + "subject_line_mastodon": "マストドンふう: そのままコピー", + "subject_line_noop": "コピーしない", "stop_gifs": "カーソルをかさねたとき、GIFをうごかす", "streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする", "text": "もじ", "theme": "テーマ", "theme_help": "カラーテーマをカスタマイズできます", + "theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、いろと、とうめいどを、オーバーライドできます。「すべてクリア」ボタンをおすと、すべてのオーバーライドを、やめます。", + "theme_help_v2_2": "バックグラウンドとテキストのコントラストをあらわすアイコンがあります。マウスをホバーすると、くわしいせつめいがでます。とうめいないろをつかっているときは、もっともわるいばあいのコントラストがしめされます。", "tooltipRadius": "ツールチップとアラート", "user_settings": "ユーザーせってい", "values": { "false": "いいえ", "true": "はい" + }, + "notifications": "つうち", + "enable_web_push_notifications": "ウェブプッシュつうちをゆるす", + "style": { + "switcher": { + "keep_color": "いろをのこす", + "keep_shadows": "かげをのこす", + "keep_opacity": "とうめいどをのこす", + "keep_roundness": "まるさをのこす", + "keep_fonts": "フォントをのこす", + "save_load_hint": "「のこす」オプションをONにすると、テーマをえらんだときとロードしたとき、いまのせっていをのこします。また、テーマをエクスポートするとき、これらのオプションをストアします。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべてのせっていをセーブします。", + "reset": "リセット", + "clear_all": "すべてクリア", + "clear_opacity": "とうめいどをクリア" + }, + "common": { + "color": "いろ", + "opacity": "とうめいど", + "contrast": { + "hint": "コントラストは {ratio} です。{level}。({context})", + "level": { + "aa": "AAレベルガイドライン (ミニマル) をみたします", + "aaa": "AAAレベルガイドライン (レコメンデッド) をみたします。", + "bad": "ガイドラインをみたしません。" + }, + "context": { + "18pt": "おおきい (18ポイントいじょう) テキスト", + "text": "テキスト" + } + } + }, + "common_colors": { + "_tab_label": "きょうつう", + "main": "きょうつうのいろ", + "foreground_hint": "「くわしく」タブで、もっとこまかくせっていできます", + "rgbo": "アイコンとアクセントとバッジ" + }, + "advanced_colors": { + "_tab_label": "くわしく", + "alert": "アラートのバックグラウンド", + "alert_error": "エラー", + "badge": "バッジのバックグラウンド", + "badge_notification": "つうち", + "panel_header": "パネルヘッダー", + "top_bar": "トップバー", + "borders": "さかいめ", + "buttons": "ボタン", + "inputs": "インプットフィールド", + "faint_text": "うすいテキスト" + }, + "radii": { + "_tab_label": "まるさ" + }, + "shadows": { + "_tab_label": "ひかりとかげ", + "component": "コンポーネント", + "override": "オーバーライド", + "shadow_id": "かげ #{value}", + "blur": "ぼかし", + "spread": "ひろがり", + "inset": "うちがわ", + "hint": "かげのせっていでは、いろのあたいとして --variable をつかうことができます。これはCSS3へんすうです。ただし、とうめいどのせっていは、きかなくなります。", + "filter_hint": { + "always_drop_shadow": "ブラウザーがサポートしていれば、つねに {0} がつかわれます。", + "drop_shadow_syntax": "{0} は、{1} パラメーターと {2} キーワードをサポートしていません。", + "avatar_inset": "うちがわのかげと、そとがわのかげを、いっしょにつかうと、とうめいなアバターが、へんなみためになります。", + "spread_zero": "ひろがりが 0 よりもおおきなかげは、0 とおなじです。", + "inset_classic": "うちがわのかげは {0} をつかいます。" + }, + "components": { + "panel": "パネル", + "panelHeader": "パネルヘッダー", + "topBar": "トップバー", + "avatar": "ユーザーアバター (プロフィール)", + "avatarStatus": "ユーザーアバター (とうこう)", + "popup": "ポップアップとツールチップ", + "button": "ボタン", + "buttonHover": "ボタン (ホバー)", + "buttonPressed": "ボタン (おされているとき)", + "buttonPressedHover": "ボタン (ホバー、かつ、おされているとき)", + "input": "インプットフィールド" + } + }, + "fonts": { + "_tab_label": "フォント", + "help": "「カスタム」をえらんだときは、システムにあるフォントのなまえを、ただしくにゅうりょくしてください。", + "components": { + "interface": "インターフェース", + "input": "インプットフィールド", + "post": "とうこう", + "postCode": "モノスペース (とうこうがリッチテキストであるとき)" + }, + "family": "フォントめい", + "size": "おおきさ (px)", + "weight": "ふとさ", + "custom": "カスタム" + }, + "preview": { + "header": "プレビュー", + "content": "ほんぶん", + "error": "エラーのれい", + "button": "ボタン", + "text": "これは{0}と{1}のれいです。", + "mono": "monospace", + "input": "はねだくうこうに、つきました。", + "faint_link": "とてもたすけになるマニュアル", + "fine_print": "わたしたちの{0}を、よまないでください!", + "header_faint": "エラーではありません", + "checkbox": "りようきやくを、よみました", + "link": "ハイパーリンク" + } } }, "timeline": { @@ -183,10 +322,15 @@ "blocked": "ブロックしています!", "deny": "おことわり", "follow": "フォロー", + "follow_sent": "リクエストを、おくりました!", + "follow_progress": "リクエストしています…", + "follow_again": "ふたたびリクエストをおくりますか?", + "follow_unfollow": "フォローをやめる", "followees": "フォロー", "followers": "フォロワー", "following": "フォローしています!", "follows_you": "フォローされました!", + "its_you": "これはあなたです!", "mute": "ミュート", "muted": "ミュートしています!", "per_day": "/日", @@ -199,5 +343,26 @@ "who_to_follow": { "more": "くわしく", "who_to_follow": "おすすめユーザー" + }, + "tool_tip": { + "media_upload": "メディアをアップロード", + "repeat": "リピート", + "reply": "リプライ", + "favorite": "おきにいり", + "user_settings": "ユーザーせってい" + }, + "upload":{ + "error": { + "base": "アップロードにしっぱいしました。", + "file_too_big": "ファイルがおおきすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", + "default": "しばらくしてから、ためしてください" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } } } diff --git a/src/lib/push_notifications_plugin.js b/src/lib/push_notifications_plugin.js new file mode 100644 index 00000000..f75bb823 --- /dev/null +++ b/src/lib/push_notifications_plugin.js @@ -0,0 +1,22 @@ +export default (store) => { + store.subscribe((mutation, state) => { + const vapidPublicKey = state.instance.vapidPublicKey + const webPushNotification = state.config.webPushNotifications + const permission = state.interface.notificationPermission === 'granted' + const user = state.users.currentUser + + const isUserMutation = mutation.type === 'setCurrentUser' + const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey' + const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted' + const isUserConfigMutation = mutation.type === 'setOption' && mutation.payload.name === 'webPushNotifications' + const isVisibilityMutation = mutation.type === 'setOption' && mutation.payload.name === 'notificationVisibility' + + if (isUserMutation || isVapidMutation || isPermMutation || isUserConfigMutation || isVisibilityMutation) { + if (user && vapidPublicKey && permission && webPushNotification) { + return store.dispatch('registerPushNotifications') + } else if (isUserConfigMutation && !webPushNotification) { + return store.dispatch('unregisterPushNotifications') + } + } + }) +} diff --git a/src/main.js b/src/main.js index bf92e78e..f87ef9da 100644 --- a/src/main.js +++ b/src/main.js @@ -15,6 +15,7 @@ import VueTimeago from 'vue-timeago' import VueI18n from 'vue-i18n' import createPersistedState from './lib/persisted_state.js' +import pushNotifications from './lib/push_notifications_plugin.js' import messages from './i18n/messages.js' @@ -51,31 +52,6 @@ const persistedStateOptions = { ] } -const registerPushNotifications = store => { - store.subscribe((mutation, state) => { - const vapidPublicKey = state.instance.vapidPublicKey - const permission = state.interface.notificationPermission === 'granted' - const isUserMutation = mutation.type === 'setCurrentUser' - - if (isUserMutation && vapidPublicKey && permission) { - return store.dispatch('registerPushNotifications') - } - - const user = state.users.currentUser - const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey' - - if (isVapidMutation && user && permission) { - return store.dispatch('registerPushNotifications') - } - - const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted' - - if (isPermMutation && user && vapidPublicKey) { - return store.dispatch('registerPushNotifications') - } - }) -} - createPersistedState(persistedStateOptions).then((persistedState) => { const store = new Vuex.Store({ modules: { @@ -88,7 +64,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => { chat: chatModule, oauth: oauthModule }, - plugins: [persistedState, registerPushNotifications], + plugins: [persistedState, pushNotifications], strict: false // Socket modifies itself, let's ignore this for now. // strict: process.env.NODE_ENV !== 'production' }) diff --git a/src/modules/config.js b/src/modules/config.js index ccfd0190..c9528f6f 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -24,7 +24,7 @@ const defaultState = { likes: true, repeats: true }, - webPushNotifications: true, + webPushNotifications: false, muteWords: [], highlight: {}, interfaceLanguage: browserLocale, diff --git a/src/modules/instance.js b/src/modules/instance.js index 342bc9ac..546d2cc4 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -27,6 +27,7 @@ const defaultState = { loginMethod: 'password', nsfwCensorImage: undefined, vapidPublicKey: undefined, + noAttachmentLinks: false, // Nasty stuff pleromaBackend: true, diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 8c2d36bc..dccccf72 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -27,6 +27,7 @@ export const defaultState = { maxId: 0, minId: Number.POSITIVE_INFINITY, data: [], + idStore: {}, error: false }, favorites: new Set(), @@ -307,6 +308,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot } state.notifications.data.push(result) + state.notifications.idStore[notification.id] = result if ('Notification' in window && window.Notification.permission === 'granted') { const title = action.user.name diff --git a/src/modules/users.js b/src/modules/users.js index 13d3f26e..2f05ed3f 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,7 +1,7 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { compact, map, each, merge } from 'lodash' import { set } from 'vue' -import registerPushNotifications from '../services/push/push.js' +import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' import oauthApi from '../services/new_api/oauth' import { humanizeErrors } from './errors' @@ -66,6 +66,9 @@ export const mutations = { setUserForStatus (state, status) { status.user = state.usersObject[status.user.id] }, + setUserForNotification (state, notification) { + notification.action.user = state.usersObject[notification.action.user.id] + }, setColor (state, { user: { id }, highlighted }) { const user = state.usersObject[id] set(user, 'highlight', highlighted) @@ -113,8 +116,14 @@ const users = { const token = store.state.currentUser.credentials const vapidPublicKey = store.rootState.instance.vapidPublicKey const isEnabled = store.rootState.config.webPushNotifications + const notificationVisibility = store.rootState.config.notificationVisibility - registerPushNotifications(isEnabled, vapidPublicKey, token) + registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility) + }, + unregisterPushNotifications (store) { + const token = store.state.currentUser.credentials + + unregisterPushNotifications(token) }, addNewStatuses (store, { statuses }) { const users = map(statuses, 'user') @@ -131,6 +140,21 @@ const users = { store.commit('setUserForStatus', status) }) }, + addNewNotifications (store, { notifications }) { + const users = compact(map(notifications, 'from_profile')) + const notificationIds = compact(notifications.map(_ => String(_.id))) + store.commit('addNewUsers', users) + + const notificationsObject = store.rootState.statuses.notifications.idStore + const relevantNotifications = Object.entries(notificationsObject) + .filter(([k, val]) => notificationIds.includes(k)) + .map(([k, val]) => val) + + // Reconnect users to notifications + each(relevantNotifications, (notification) => { + store.commit('setUserForNotification', notification) + }) + }, async signUp (store, userInfo) { store.commit('signUpPending') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 182f9126..4ee95bd1 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -370,12 +370,13 @@ const unretweet = ({ id, credentials }) => { }) } -const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) => { +const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks}) => { const idsText = mediaIds.join(',') 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) diff --git a/src/services/push/push.js b/src/services/push/push.js index 1ac304d1..1b189a29 100644 --- a/src/services/push/push.js +++ b/src/services/push/push.js @@ -14,12 +14,12 @@ function isPushSupported () { return 'serviceWorker' in navigator && 'PushManager' in window } -function registerServiceWorker () { +function getOrCreateServiceWorker () { return runtime.register() - .catch((err) => console.error('Unable to register service worker.', err)) + .catch((err) => console.error('Unable to get or create a service worker.', err)) } -function subscribe (registration, isEnabled, vapidPublicKey) { +function subscribePush (registration, isEnabled, vapidPublicKey) { if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config')) if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found')) @@ -30,7 +30,28 @@ function subscribe (registration, isEnabled, vapidPublicKey) { return registration.pushManager.subscribe(subscribeOptions) } -function sendSubscriptionToBackEnd (subscription, token) { +function unsubscribePush (registration) { + return registration.pushManager.getSubscription() + .then((subscribtion) => { + if (subscribtion === null) { return } + return subscribtion.unsubscribe() + }) +} + +function deleteSubscriptionFromBackEnd (token) { + return window.fetch('/api/v1/push/subscription/', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }).then((response) => { + if (!response.ok) throw new Error('Bad status code from server.') + return response + }) +} + +function sendSubscriptionToBackEnd (subscription, token, notificationVisibility) { return window.fetch('/api/v1/push/subscription/', { method: 'POST', headers: { @@ -41,29 +62,49 @@ function sendSubscriptionToBackEnd (subscription, token) { subscription, data: { alerts: { - follow: true, - favourite: true, - mention: true, - reblog: true + follow: notificationVisibility.follows, + favourite: notificationVisibility.likes, + mention: notificationVisibility.mentions, + reblog: notificationVisibility.repeats } } }) + }).then((response) => { + if (!response.ok) throw new Error('Bad status code from server.') + return response.json() + }).then((responseData) => { + if (!responseData.id) throw new Error('Bad response from server.') + return responseData }) - .then((response) => { - if (!response.ok) throw new Error('Bad status code from server.') - return response.json() - }) - .then((responseData) => { - if (!responseData.id) throw new Error('Bad response from server.') - return responseData - }) } -export default function registerPushNotifications (isEnabled, vapidPublicKey, token) { +export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) { if (isPushSupported()) { - registerServiceWorker() - .then((registration) => subscribe(registration, isEnabled, vapidPublicKey)) - .then((subscription) => sendSubscriptionToBackEnd(subscription, token)) + getOrCreateServiceWorker() + .then((registration) => subscribePush(registration, isEnabled, vapidPublicKey)) + .then((subscription) => sendSubscriptionToBackEnd(subscription, token, notificationVisibility)) .catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`)) } } + +export function unregisterPushNotifications (token) { + if (isPushSupported()) { + Promise.all([ + deleteSubscriptionFromBackEnd(token), + getOrCreateServiceWorker() + .then((registration) => { + return unsubscribePush(registration).then((result) => [registration, result]) + }) + .then(([registration, unsubResult]) => { + if (!unsubResult) { + console.warn('Push subscription cancellation wasn\'t successful, killing SW anyway...') + } + return registration.unregister().then((result) => { + if (!result) { + console.warn('Failed to kill SW') + } + }) + }) + ]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`)) + } +} diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index 7f8b0fc0..1e20d336 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}) + return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks}) .then((data) => data.json()) .then((data) => { if (!data.error) { diff --git a/static/config.json b/static/config.json index 7887f930..9bdcb3b2 100644 --- a/static/config.json +++ b/static/config.json @@ -16,5 +16,7 @@ "alwaysShowSubjectInput": true, "hidePostStats": false, "hideUserStats": false, - "loginMethod": "password" + "loginMethod": "password", + "webPushNotifications": false, + "noAttachmentLinks": false } |
