fix conflict in user card content

This commit is contained in:
shpuld 2018-12-28 21:44:09 +02:00
commit 650655709d
16 changed files with 432 additions and 188 deletions

View file

@ -72,6 +72,7 @@ const afterStoreSetup = ({ store, i18n }) => {
var scopeCopy = (config.scopeCopy) var scopeCopy = (config.scopeCopy)
var subjectLineBehavior = (config.subjectLineBehavior) var subjectLineBehavior = (config.subjectLineBehavior)
var alwaysShowSubjectInput = (config.alwaysShowSubjectInput) var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
var noAttachmentLinks = (config.noAttachmentLinks)
store.dispatch('setInstanceOption', { name: 'theme', value: theme }) store.dispatch('setInstanceOption', { name: 'theme', value: theme })
store.dispatch('setInstanceOption', { name: 'background', value: background }) 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: 'scopeCopy', value: scopeCopy })
store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior }) store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput }) store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
store.dispatch('setInstanceOption', { name: 'noAttachmentLinks', value: noAttachmentLinks })
if (chatDisabled) { if (chatDisabled) {
store.dispatch('disableChat') store.dispatch('disableChat')
} }

View file

@ -32,6 +32,8 @@ const PostStatusForm = {
}, },
mounted () { mounted () {
this.resize(this.$refs.textarea) this.resize(this.$refs.textarea)
const textLength = this.$refs.textarea.value.length
this.$refs.textarea.setSelectionRange(textLength, textLength)
if (this.replyTo) { if (this.replyTo) {
this.$refs.textarea.focus() this.$refs.textarea.focus()
@ -250,7 +252,8 @@ const PostStatusForm = {
} }
this.$emit('posted') this.$emit('posted')
let el = this.$el.querySelector('textarea') let el = this.$el.querySelector('textarea')
el.style.height = '16px' el.style.height = 'auto'
el.style.height = undefined
this.error = null this.error = null
} else { } else {
this.error = data.error this.error = data.error
@ -298,13 +301,15 @@ const PostStatusForm = {
e.dataTransfer.dropEffect = 'copy' e.dataTransfer.dropEffect = 'copy'
}, },
resize (e) { resize (e) {
if (!e.target) { return } const target = e.target || e
const vertPadding = Number(window.getComputedStyle(e.target)['padding-top'].substr(0, 1)) + if (!(target instanceof window.Element)) { return }
Number(window.getComputedStyle(e.target)['padding-bottom'].substr(0, 1)) const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
e.target.style.height = 'auto' Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
e.target.style.height = `${e.target.scrollHeight - vertPadding}px` // Auto is needed to make textbox shrink when removing lines
if (e.target.value === '') { target.style.height = 'auto'
e.target.style.height = '16px' target.style.height = `${target.scrollHeight - vertPadding}px`
if (target.value === '') {
target.style.height = null
} }
}, },
clearError () { clearError () {

View file

@ -2,22 +2,25 @@
<div id="heading" class="profile-panel-background" :style="headingStyle"> <div id="heading" class="profile-panel-background" :style="headingStyle">
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<div class='user-info'> <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'> <div class='container'>
<router-link :to="userProfileLink(user)"> <router-link :to="userProfileLink(user)">
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/> <StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
</router-link> </router-link>
<div class="name-and-screen-name"> <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 class="top-line">
<div :title="user.name" class='user-name' v-else>{{user.name}}</div> <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)"> <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 class="handle">@{{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 v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link> </router-link>
</div> </div>
</div> </div>
@ -25,7 +28,7 @@
<div v-if="user.follows_you && loggedIn && isOtherUser" class="following"> <div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
{{ $t('user_card.follows_you') }} {{ $t('user_card.follows_you') }}
</div> </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 --> <!-- 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="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"/> <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; border-bottom-right-radius: 0;
.panel-heading { .panel-heading {
padding: 0.6em 0em; padding: .6em 0;
text-align: center; text-align: center;
box-shadow: none; box-shadow: none;
} }
@ -158,10 +161,10 @@
.user-info { .user-info {
color: $fallback--lightText; color: $fallback--lightText;
color: var(--lightText, $fallback--lightText); color: var(--lightText, $fallback--lightText);
padding: 0 16px; padding: 0 26px;
.container { .container {
padding: 16px 10px 6px 10px; padding: 16px 0 6px;
display: flex; display: flex;
max-height: 56px; max-height: 56px;
@ -218,11 +221,15 @@
vertical-align: middle; vertical-align: middle;
object-fit: contain object-fit: contain
} }
.top-line {
display: flex;
}
} }
.user-name{ .user-name{
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
flex: 1 0 auto;
} }
.user-screen-name { .user-screen-name {
@ -232,27 +239,73 @@
font-weight: light; font-weight: light;
font-size: 15px; font-size: 15px;
padding-right: 0.1em; 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 { .user-meta {
margin-bottom: .4em; margin-bottom: .15em;
display: flex;
align-items: baseline;
font-size: 14px;
line-height: 22px;
flex-wrap: wrap;
.following { .following {
font-size: 14px; flex: 1 0 auto;
flex: 0 0 100%;
margin: 0; margin: 0;
padding-left: 16px; margin-bottom: .25em;
text-align: left; text-align: left;
float: left;
}
.floater {
margin: 0;
} }
&::after { .highlighter {
display: block; flex: 0 1 auto;
content: ''; display: flex;
clear: both; 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 { .user-interactions {
@ -260,8 +313,13 @@
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
margin-right: -.75em;
div { div {
flex: 1; flex: 1 0 0;
margin-right: .75em;
margin-bottom: .6em;
white-space: nowrap;
} }
.mute { .mute {
@ -280,8 +338,9 @@
} }
button { button {
width: 92%; width: 100%;
height: 100%; height: 100%;
margin: 0;
} }
.remote-button { .remote-button {
@ -304,10 +363,11 @@
justify-content: space-between; justify-content: space-between;
color: $fallback--lightText; color: $fallback--lightText;
color: var(--lightText, $fallback--lightText); color: var(--lightText, $fallback--lightText);
flex-wrap: wrap;
} }
.user-count { .user-count {
flex: 1; flex: 1 0 auto;
padding: .5em 0 .5em 0; padding: .5em 0 .5em 0;
margin: 0 .5em; margin: 0 .5em;
@ -327,32 +387,5 @@
color: #CCC; color: #CCC;
} }
.floater { .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> </style>

View file

@ -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' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
function showWhoToFollow (panel, reply) { function showWhoToFollow (panel, reply) {
var users = reply panel.usersToFollow.forEach((toFollow, index) => {
var cn let randIndex = Math.floor(Math.random() * reply.length)
var index let user = reply[randIndex]
var step = 7 let img = user.avatar || '/images/avi.png'
cn = Math.floor(Math.random() * step) let name = user.acct
for (index = 0; index < 3; index++) {
var user toFollow.img = img
user = users[cn] toFollow.name = name
var img
if (user.avatar) { panel.$store.state.api.backendInteractor.externalProfile(name)
img = user.avatar .then((externalUser) => {
} else { if (!externalUser.error) {
img = '/images/avi.png' panel.$store.commit('addNewUsers', [externalUser])
} toFollow.id = externalUser.id
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
}
} }
function getWhoToFollow (panel) { function getWhoToFollow (panel) {
var credentials = panel.$store.state.users.currentUser.credentials var credentials = panel.$store.state.users.currentUser.credentials
if (credentials) { if (credentials) {
panel.name1 = 'Loading...' panel.usersToFollow.forEach(toFollow => {
panel.name2 = 'Loading...' toFollow.name = 'Loading...'
panel.name3 = 'Loading...' })
apiService.suggestions({credentials: credentials}) apiService.suggestions({credentials: credentials})
.then((reply) => { .then((reply) => {
showWhoToFollow(panel, reply) showWhoToFollow(panel, reply)
@ -67,27 +36,24 @@ function getWhoToFollow (panel) {
const WhoToFollowPanel = { const WhoToFollowPanel = {
data: () => ({ data: () => ({
img1: '/images/avi.png', usersToFollow: new Array(3).fill().map(x => (
name1: '', {
id1: 0, img: '/images/avi.png',
img2: '/images/avi.png', name: '',
name2: '', id: 0
id2: 0, }
img3: '/images/avi.png', ))
name3: '',
id3: 0
}), }),
computed: { computed: {
user: function () { user: function () {
return this.$store.state.users.currentUser.screen_name return this.$store.state.users.currentUser.screen_name
}, },
moreUrl: function () { moreUrl: function () {
var host = window.location.hostname const host = window.location.hostname
var user = this.user const user = this.user
var suggestionsWeb = this.$store.state.instance.suggestionsWeb const suggestionsWeb = this.$store.state.instance.suggestionsWeb
var url const url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host)) .replace(/{{user}}/g, encodeURIComponent(user))
url = url.replace(/{{user}}/g, encodeURIComponent(user))
return url return url
}, },
suggestionsEnabled () { suggestionsEnabled () {

View file

@ -8,9 +8,12 @@
</div> </div>
<div class="panel-body who-to-follow"> <div class="panel-body who-to-follow">
<p> <p>
<img v-bind:src="img1"/> <router-link :to="userProfileLink(id1, name1)">{{ name1 }}</router-link><br> <span v-for="user in usersToFollow">
<img v-bind:src="img2"/> <router-link :to="userProfileLink(id2, name2)">{{ name2 }}</router-link><br> <img v-bind:src="user.img" />
<img v-bind:src="img3"/> <router-link :to="userProfileLink(id3, name3)">{{ name3 }}</router-link><br> <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> <img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
</p> </p>
</div> </div>

View file

@ -29,13 +29,16 @@
"username": "ユーザーめい" "username": "ユーザーめい"
}, },
"nav": { "nav": {
"back": "もどる",
"chat": "ローカルチャット", "chat": "ローカルチャット",
"friend_requests": "フォローリクエスト", "friend_requests": "フォローリクエスト",
"mentions": "メンション", "mentions": "メンション",
"dms": "ダイレクトメッセージ", "dms": "ダイレクトメッセージ",
"public_tl": "パブリックタイムライン", "public_tl": "パブリックタイムライン",
"timeline": "タイムライン", "timeline": "タイムライン",
"twkn": "つながっているすべてのネットワーク" "twkn": "つながっているすべてのネットワーク",
"user_search": "ユーザーをさがす",
"preferences": "せってい"
}, },
"notifications": { "notifications": {
"broken_favorite": "ステータスがみつかりません。さがしています...", "broken_favorite": "ステータスがみつかりません。さがしています...",
@ -70,7 +73,17 @@
"fullname": "スクリーンネーム", "fullname": "スクリーンネーム",
"password_confirm": "パスワードのかくにん", "password_confirm": "パスワードのかくにん",
"registration": "はじめる", "registration": "はじめる",
"token": "しょうたいトークン" "token": "しょうたいトークン",
"captcha": "CAPTCHA",
"new_captcha": "もじがよめないときは、がぞうをクリックすると、あたらしいがぞうになります",
"validations": {
"username_required": "なにかかいてください",
"fullname_required": "なにかかいてください",
"email_required": "なにかかいてください",
"password_required": "なにかかいてください",
"password_confirmation_required": "なにかかいてください",
"password_confirmation_match": "パスワードがちがいます"
}
}, },
"settings": { "settings": {
"attachmentRadius": "ファイル", "attachmentRadius": "ファイル",
@ -90,6 +103,7 @@
"change_password_error": "パスワードをかえることが、できなかったかもしれません。", "change_password_error": "パスワードをかえることが、できなかったかもしれません。",
"changed_password": "パスワードが、かわりました!", "changed_password": "パスワードが、かわりました!",
"collapse_subject": "せつめいのあるとうこうをたたむ", "collapse_subject": "せつめいのあるとうこうをたたむ",
"composing": "とうこう",
"confirm_new_password": "あたらしいパスワードのかくにん", "confirm_new_password": "あたらしいパスワードのかくにん",
"current_avatar": "いまのアバター", "current_avatar": "いまのアバター",
"current_password": "いまのパスワード", "current_password": "いまのパスワード",
@ -113,17 +127,22 @@
"general": "ぜんぱん", "general": "ぜんぱん",
"hide_attachments_in_convo": "スレッドのファイルをかくす", "hide_attachments_in_convo": "スレッドのファイルをかくす",
"hide_attachments_in_tl": "タイムラインのファイルをかくす", "hide_attachments_in_tl": "タイムラインのファイルをかくす",
"hide_isp": "インスタンススペシフィックパネルをかくす",
"preload_images": "がぞうをさきよみする",
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)", "hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)", "hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする", "import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
"import_theme": "ロード", "import_theme": "ロード",
"inputRadius": "インプットフィールド", "inputRadius": "インプットフィールド",
"checkboxRadius": "チェックボックス",
"instance_default": "(デフォルト: {value})", "instance_default": "(デフォルト: {value})",
"instance_default_simple": "(デフォルト)",
"interface": "インターフェース",
"interfaceLanguage": "インターフェースのことば", "interfaceLanguage": "インターフェースのことば",
"invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマはへんこうされませんでした。", "invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマはへんこうされませんでした。",
"limited_availability": "あなたのブラウザではできません", "limited_availability": "あなたのブラウザではできません",
"links": "リンク", "links": "リンク",
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできます", "lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローでき",
"loop_video": "ビデオをくりかえす", "loop_video": "ビデオをくりかえす",
"loop_video_silent_only": "おとのないビデオだけくりかえす", "loop_video_silent_only": "おとのないビデオだけくりかえす",
"name": "なまえ", "name": "なまえ",
@ -135,6 +154,7 @@
"notification_visibility_mentions": "メンション", "notification_visibility_mentions": "メンション",
"notification_visibility_repeats": "リピート", "notification_visibility_repeats": "リピート",
"no_rich_text_description": "リッチテキストをつかわない", "no_rich_text_description": "リッチテキストをつかわない",
"hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない",
"nsfw_clickthrough": "NSFWなファイルをかくす", "nsfw_clickthrough": "NSFWなファイルをかくす",
"panelRadius": "パネル", "panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
@ -151,20 +171,139 @@
"saving_err": "せっていをセーブできませんでした", "saving_err": "せっていをセーブできませんでした",
"saving_ok": "せっていをセーブしました", "saving_ok": "せっていをセーブしました",
"security_tab": "セキュリティ", "security_tab": "セキュリティ",
"scope_copy": "リプライするとき、こうかいはんいをコピーする (DMのこうかいはんいは、つねにコピーされます)",
"set_new_avatar": "あたらしいアバターをせっていする", "set_new_avatar": "あたらしいアバターをせっていする",
"set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする", "set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする",
"set_new_profile_banner": "あたらしいプロフィールバナーを設定する", "set_new_profile_banner": "あたらしいプロフィールバナーを設定する",
"settings": "せってい", "settings": "せってい",
"subject_input_always_show": "サブジェクトフィールドをいつでもひょうじする",
"subject_line_behavior": "リプライするときサブジェクトをコピーする",
"subject_line_email": "メールふう: \"re: サブジェクト\"",
"subject_line_mastodon": "マストドンふう: そのままコピー",
"subject_line_noop": "コピーしない",
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす", "stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする", "streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
"text": "もじ", "text": "もじ",
"theme": "テーマ", "theme": "テーマ",
"theme_help": "カラーテーマをカスタマイズできます", "theme_help": "カラーテーマをカスタマイズできます",
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、いろと、とうめいどを、オーバーライドできます。「すべてクリア」ボタンをおすと、すべてのオーバーライドを、やめます。",
"theme_help_v2_2": "バックグラウンドとテキストのコントラストをあらわすアイコンがあります。マウスをホバーすると、くわしいせつめいがでます。とうめいないろをつかっているときは、もっともわるいばあいのコントラストがしめされます。",
"tooltipRadius": "ツールチップとアラート", "tooltipRadius": "ツールチップとアラート",
"user_settings": "ユーザーせってい", "user_settings": "ユーザーせってい",
"values": { "values": {
"false": "いいえ", "false": "いいえ",
"true": "はい" "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": { "timeline": {
@ -183,10 +322,15 @@
"blocked": "ブロックしています!", "blocked": "ブロックしています!",
"deny": "おことわり", "deny": "おことわり",
"follow": "フォロー", "follow": "フォロー",
"follow_sent": "リクエストを、おくりました!",
"follow_progress": "リクエストしています…",
"follow_again": "ふたたびリクエストをおくりますか?",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー", "followees": "フォロー",
"followers": "フォロワー", "followers": "フォロワー",
"following": "フォローしています!", "following": "フォローしています!",
"follows_you": "フォローされました!", "follows_you": "フォローされました!",
"its_you": "これはあなたです!",
"mute": "ミュート", "mute": "ミュート",
"muted": "ミュートしています!", "muted": "ミュートしています!",
"per_day": "/日", "per_day": "/日",
@ -199,5 +343,26 @@
"who_to_follow": { "who_to_follow": {
"more": "くわしく", "more": "くわしく",
"who_to_follow": "おすすめユーザー" "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"
}
} }
} }

View file

@ -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')
}
}
})
}

View file

@ -15,6 +15,7 @@ import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
import createPersistedState from './lib/persisted_state.js' import createPersistedState from './lib/persisted_state.js'
import pushNotifications from './lib/push_notifications_plugin.js'
import messages from './i18n/messages.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) => { createPersistedState(persistedStateOptions).then((persistedState) => {
const store = new Vuex.Store({ const store = new Vuex.Store({
modules: { modules: {
@ -88,7 +64,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
chat: chatModule, chat: chatModule,
oauth: oauthModule oauth: oauthModule
}, },
plugins: [persistedState, registerPushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production' // strict: process.env.NODE_ENV !== 'production'
}) })

View file

@ -24,7 +24,7 @@ const defaultState = {
likes: true, likes: true,
repeats: true repeats: true
}, },
webPushNotifications: true, webPushNotifications: false,
muteWords: [], muteWords: [],
highlight: {}, highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,

View file

@ -27,6 +27,7 @@ const defaultState = {
loginMethod: 'password', loginMethod: 'password',
nsfwCensorImage: undefined, nsfwCensorImage: undefined,
vapidPublicKey: undefined, vapidPublicKey: undefined,
noAttachmentLinks: false,
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,

View file

@ -27,6 +27,7 @@ export const defaultState = {
maxId: 0, maxId: 0,
minId: Number.POSITIVE_INFINITY, minId: Number.POSITIVE_INFINITY,
data: [], data: [],
idStore: {},
error: false error: false
}, },
favorites: new Set(), favorites: new Set(),
@ -307,6 +308,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
} }
state.notifications.data.push(result) state.notifications.data.push(result)
state.notifications.idStore[notification.id] = result
if ('Notification' in window && window.Notification.permission === 'granted') { if ('Notification' in window && window.Notification.permission === 'granted') {
const title = action.user.name const title = action.user.name

View file

@ -1,7 +1,7 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { compact, map, each, merge } from 'lodash' import { compact, map, each, merge } from 'lodash'
import { set } from 'vue' 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 oauthApi from '../services/new_api/oauth'
import { humanizeErrors } from './errors' import { humanizeErrors } from './errors'
@ -66,6 +66,9 @@ export const mutations = {
setUserForStatus (state, status) { setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id] status.user = state.usersObject[status.user.id]
}, },
setUserForNotification (state, notification) {
notification.action.user = state.usersObject[notification.action.user.id]
},
setColor (state, { user: { id }, highlighted }) { setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
set(user, 'highlight', highlighted) set(user, 'highlight', highlighted)
@ -113,8 +116,14 @@ const users = {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey const vapidPublicKey = store.rootState.instance.vapidPublicKey
const isEnabled = store.rootState.config.webPushNotifications 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 }) { addNewStatuses (store, { statuses }) {
const users = map(statuses, 'user') const users = map(statuses, 'user')
@ -131,6 +140,21 @@ const users = {
store.commit('setUserForStatus', status) 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) { async signUp (store, userInfo) {
store.commit('signUpPending') store.commit('signUpPending')

View file

@ -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 idsText = mediaIds.join(',')
const form = new FormData() const form = new FormData()
form.append('status', status) form.append('status', status)
form.append('source', 'Pleroma FE') form.append('source', 'Pleroma FE')
if (noAttachmentLinks) form.append('no_attachment_links', noAttachmentLinks)
if (spoilerText) form.append('spoiler_text', spoilerText) if (spoilerText) form.append('spoiler_text', spoilerText)
if (visibility) form.append('visibility', visibility) if (visibility) form.append('visibility', visibility)
if (sensitive) form.append('sensitive', sensitive) if (sensitive) form.append('sensitive', sensitive)

View file

@ -14,12 +14,12 @@ function isPushSupported () {
return 'serviceWorker' in navigator && 'PushManager' in window return 'serviceWorker' in navigator && 'PushManager' in window
} }
function registerServiceWorker () { function getOrCreateServiceWorker () {
return runtime.register() 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 (!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')) 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) 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/', { return window.fetch('/api/v1/push/subscription/', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -41,29 +62,49 @@ function sendSubscriptionToBackEnd (subscription, token) {
subscription, subscription,
data: { data: {
alerts: { alerts: {
follow: true, follow: notificationVisibility.follows,
favourite: true, favourite: notificationVisibility.likes,
mention: true, mention: notificationVisibility.mentions,
reblog: true 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()) { if (isPushSupported()) {
registerServiceWorker() getOrCreateServiceWorker()
.then((registration) => subscribe(registration, isEnabled, vapidPublicKey)) .then((registration) => subscribePush(registration, isEnabled, vapidPublicKey))
.then((subscription) => sendSubscriptionToBackEnd(subscription, token)) .then((subscription) => sendSubscriptionToBackEnd(subscription, token, notificationVisibility))
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`)) .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}`))
}
}

View file

@ -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 postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
const mediaIds = map(media, 'id') 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) => data.json())
.then((data) => { .then((data) => {
if (!data.error) { if (!data.error) {

View file

@ -16,5 +16,7 @@
"alwaysShowSubjectInput": true, "alwaysShowSubjectInput": true,
"hidePostStats": false, "hidePostStats": false,
"hideUserStats": false, "hideUserStats": false,
"loginMethod": "password" "loginMethod": "password",
"webPushNotifications": false,
"noAttachmentLinks": false
} }