aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/about/about.vue6
-rw-r--r--src/components/attachment/attachment.js2
-rw-r--r--src/components/attachment/attachment.vue103
-rw-r--r--src/components/autosuggest/autosuggest.js4
-rw-r--r--src/components/autosuggest/autosuggest.vue22
-rw-r--r--src/components/avatar_list/avatar_list.vue12
-rw-r--r--src/components/basic_user_card/basic_user_card.vue44
-rw-r--r--src/components/block_card/block_card.vue14
-rw-r--r--src/components/chat_panel/chat_panel.js2
-rw-r--r--src/components/chat_panel/chat_panel.vue59
-rw-r--r--src/components/checkbox/checkbox.vue9
-rw-r--r--src/components/color_input/color_input.vue65
-rw-r--r--src/components/contrast_ratio/contrast_ratio.vue52
-rw-r--r--src/components/conversation-page/conversation-page.vue4
-rw-r--r--src/components/conversation/conversation.js17
-rw-r--r--src/components/conversation/conversation.vue27
-rw-r--r--src/components/dialog_modal/dialog_modal.vue16
-rw-r--r--src/components/dm_timeline/dm_timeline.vue6
-rw-r--r--src/components/emoji-input/emoji-input.js227
-rw-r--r--src/components/emoji-input/emoji-input.vue135
-rw-r--r--src/components/emoji-input/suggestor.js94
-rw-r--r--src/components/export_import/export_import.vue33
-rw-r--r--src/components/exporter/exporter.vue12
-rw-r--r--src/components/extra_buttons/extra_buttons.js36
-rw-r--r--src/components/extra_buttons/extra_buttons.vue69
-rw-r--r--src/components/favorite_button/favorite_button.js4
-rw-r--r--src/components/favorite_button/favorite_button.vue17
-rw-r--r--src/components/features_panel/features_panel.js4
-rw-r--r--src/components/features_panel/features_panel.vue22
-rw-r--r--src/components/follow_card/follow_card.vue19
-rw-r--r--src/components/follow_request_card/follow_request_card.vue14
-rw-r--r--src/components/follow_requests/follow_requests.vue9
-rw-r--r--src/components/font_control/font_control.vue81
-rw-r--r--src/components/friends_timeline/friends_timeline.vue6
-rw-r--r--src/components/gallery/gallery.vue31
-rw-r--r--src/components/image_cropper/image_cropper.vue55
-rw-r--r--src/components/importer/importer.vue33
-rw-r--r--src/components/instance_specific_panel/instance_specific_panel.js3
-rw-r--r--src/components/instance_specific_panel/instance_specific_panel.vue10
-rw-r--r--src/components/interactions/interactions.js4
-rw-r--r--src/components/interactions/interactions.vue27
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue70
-rw-r--r--src/components/link-preview/link-preview.js14
-rw-r--r--src/components/link-preview/link-preview.vue20
-rw-r--r--src/components/list/list.vue16
-rw-r--r--src/components/login_form/login_form.js7
-rw-r--r--src/components/login_form/login_form.vue114
-rw-r--r--src/components/media_modal/media_modal.vue22
-rw-r--r--src/components/media_upload/media_upload.js6
-rw-r--r--src/components/media_upload/media_upload.vue32
-rw-r--r--src/components/mentions/mentions.vue6
-rw-r--r--src/components/mfa_form/recovery_form.js2
-rw-r--r--src/components/mfa_form/recovery_form.vue85
-rw-r--r--src/components/mfa_form/totp_form.js2
-rw-r--r--src/components/mfa_form/totp_form.vue90
-rw-r--r--src/components/mobile_nav/mobile_nav.js4
-rw-r--r--src/components/mobile_nav/mobile_nav.vue72
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.js4
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.vue53
-rw-r--r--src/components/moderation_tools/moderation_tools.js19
-rw-r--r--src/components/moderation_tools/moderation_tools.vue237
-rw-r--r--src/components/mute_card/mute_card.vue14
-rw-r--r--src/components/nav_panel/nav_panel.vue17
-rw-r--r--src/components/notification/notification.js6
-rw-r--r--src/components/notification/notification.vue99
-rw-r--r--src/components/notifications/notifications.vue66
-rw-r--r--src/components/oauth_callback/oauth_callback.js3
-rw-r--r--src/components/opacity_input/opacity_input.vue58
-rw-r--r--src/components/poll/poll.js112
-rw-r--r--src/components/poll/poll.vue134
-rw-r--r--src/components/poll/poll_form.js121
-rw-r--r--src/components/poll/poll_form.vue163
-rw-r--r--src/components/popper/popper.scss147
-rw-r--r--src/components/post_status_form/post_status_form.js210
-rw-r--r--src/components/post_status_form/post_status_form.vue413
-rw-r--r--src/components/progress_button/progress_button.vue7
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.vue6
-rw-r--r--src/components/public_timeline/public_timeline.vue6
-rw-r--r--src/components/range_input/range_input.vue79
-rw-r--r--src/components/registration/registration.js4
-rw-r--r--src/components/registration/registration.vue228
-rw-r--r--src/components/remote_follow/remote_follow.vue22
-rw-r--r--src/components/retweet_button/retweet_button.js4
-rw-r--r--src/components/retweet_button/retweet_button.vue23
-rw-r--r--src/components/scope_selector/scope_selector.js8
-rw-r--r--src/components/scope_selector/scope_selector.vue59
-rw-r--r--src/components/search/search.js98
-rw-r--r--src/components/search/search.vue208
-rw-r--r--src/components/search_bar/search_bar.js32
-rw-r--r--src/components/search_bar/search_bar.vue73
-rw-r--r--src/components/selectable_list/selectable_list.vue47
-rw-r--r--src/components/settings/settings.vue730
-rw-r--r--src/components/shadow_control/shadow_control.vue293
-rw-r--r--src/components/side_drawer/side_drawer.vue101
-rw-r--r--src/components/status/status.js48
-rw-r--r--src/components/status/status.vue423
-rw-r--r--src/components/sticker_picker/sticker_picker.js52
-rw-r--r--src/components/sticker_picker/sticker_picker.vue62
-rw-r--r--src/components/still-image/still-image.vue18
-rw-r--r--src/components/style_switcher/preview.vue151
-rw-r--r--src/components/style_switcher/style_switcher.vue827
-rw-r--r--src/components/tab_switcher/tab_switcher.js78
-rw-r--r--src/components/tab_switcher/tab_switcher.scss6
-rw-r--r--src/components/tag_timeline/tag_timeline.vue9
-rw-r--r--src/components/terms_of_service_panel/terms_of_service_panel.vue8
-rw-r--r--src/components/timeago/timeago.vue51
-rw-r--r--src/components/timeline/timeline.js33
-rw-r--r--src/components/timeline/timeline.vue81
-rw-r--r--src/components/user_avatar/user_avatar.js2
-rw-r--r--src/components/user_avatar/user_avatar.vue4
-rw-r--r--src/components/user_card/user_card.js38
-rw-r--r--src/components/user_card/user_card.vue429
-rw-r--r--src/components/user_finder/user_finder.js20
-rw-r--r--src/components/user_finder/user_finder.vue44
-rw-r--r--src/components/user_panel/user_panel.vue20
-rw-r--r--src/components/user_profile/user_profile.js74
-rw-r--r--src/components/user_profile/user_profile.vue150
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue100
-rw-r--r--src/components/user_search/user_search.js51
-rw-r--r--src/components/user_search/user_search.vue37
-rw-r--r--src/components/user_settings/confirm.vue26
-rw-r--r--src/components/user_settings/mfa.js5
-rw-r--r--src/components/user_settings/mfa.vue186
-rw-r--r--src/components/user_settings/mfa_backup_codes.vue27
-rw-r--r--src/components/user_settings/mfa_totp.vue56
-rw-r--r--src/components/user_settings/user_settings.js63
-rw-r--r--src/components/user_settings/user_settings.vue529
-rw-r--r--src/components/video_attachment/video_attachment.vue5
-rw-r--r--src/components/who_to_follow/who_to_follow.js5
-rw-r--r--src/components/who_to_follow/who_to_follow.vue9
-rw-r--r--src/components/who_to_follow_panel/who_to_follow_panel.js2
-rw-r--r--src/components/who_to_follow_panel/who_to_follow_panel.vue18
132 files changed, 6846 insertions, 2776 deletions
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index 13dec87c..62ae16ea 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -1,8 +1,8 @@
<template>
<div class="sidebar">
- <instance-specific-panel></instance-specific-panel>
- <features-panel v-if="showFeaturesPanel"></features-panel>
- <terms-of-service-panel></terms-of-service-panel>
+ <instance-specific-panel />
+ <features-panel v-if="showFeaturesPanel" />
+ <terms-of-service-panel />
</div>
</template>
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 3b7f08dc..e93921fe 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -51,7 +51,7 @@ const Attachment = {
}
},
methods: {
- linkClicked ({target}) {
+ linkClicked ({ target }) {
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index c58bebd3..ec326c45 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -1,54 +1,106 @@
<template>
- <div v-if="usePlaceHolder" @click="openModal">
- <a class="placeholder"
+ <div
+ v-if="usePlaceHolder"
+ @click="openModal"
+ >
+ <a
v-if="type !== 'html'"
- target="_blank" :href="attachment.url"
+ class="placeholder"
+ target="_blank"
+ :href="attachment.url"
>
- [{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]
+ [{{ nsfw ? "NSFW/" : "" }}{{ type.toUpperCase() }}]
</a>
</div>
<div
- v-else class="attachment"
- :class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
+ v-else
v-show="!isEmpty"
+ class="attachment"
+ :class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
>
- <a class="image-attachment" v-if="hidden" :href="attachment.url" @click.prevent="toggleHidden">
- <img class="nsfw" :key="nsfwImage" :src="nsfwImage" :class="{'small': isSmall}"/>
- <i v-if="type === 'video'" class="play-icon icon-play-circled"></i>
+ <a
+ v-if="hidden"
+ class="image-attachment"
+ :href="attachment.url"
+ @click.prevent="toggleHidden"
+ >
+ <img
+ :key="nsfwImage"
+ class="nsfw"
+ :src="nsfwImage"
+ :class="{'small': isSmall}"
+ >
+ <i
+ v-if="type === 'video'"
+ class="play-icon icon-play-circled"
+ />
</a>
- <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
- <a href="#" @click.prevent="toggleHidden">Hide</a>
+ <div
+ v-if="nsfw && hideNsfwLocal && !hidden"
+ class="hider"
+ >
+ <a
+ href="#"
+ @click.prevent="toggleHidden"
+ >Hide</a>
</div>
- <a v-if="type === 'image' && (!hidden || preloadImage)"
- @click="openModal"
+ <a
+ v-if="type === 'image' && (!hidden || preloadImage)"
class="image-attachment"
:class="{'hidden': hidden && preloadImage }"
- :href="attachment.url" target="_blank"
+ :href="attachment.url"
+ target="_blank"
:title="attachment.description"
+ @click="openModal"
>
- <StillImage :referrerpolicy="referrerpolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
+ <StillImage
+ :referrerpolicy="referrerpolicy"
+ :mimetype="attachment.mimetype"
+ :src="attachment.large_thumb_url || attachment.url"
+ />
</a>
- <a class="video-container"
- @click="openModal"
+ <a
v-if="type === 'video' && !hidden"
+ class="video-container"
:class="{'small': isSmall}"
:href="allowPlay ? undefined : attachment.url"
+ @click="openModal"
>
- <VideoAttachment class="video" :attachment="attachment" :controls="allowPlay" />
- <i v-if="!allowPlay" class="play-icon icon-play-circled"></i>
+ <VideoAttachment
+ class="video"
+ :attachment="attachment"
+ :controls="allowPlay"
+ />
+ <i
+ v-if="!allowPlay"
+ class="play-icon icon-play-circled"
+ />
</a>
- <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
+ <audio
+ v-if="type === 'audio'"
+ :src="attachment.url"
+ controls
+ />
- <div @click.prevent="linkClicked" v-if="type === 'html' && attachment.oembed" class="oembed">
- <div v-if="attachment.thumb_url" class="image">
- <img :src="attachment.thumb_url"/>
+ <div
+ v-if="type === 'html' && attachment.oembed"
+ class="oembed"
+ @click.prevent="linkClicked"
+ >
+ <div
+ v-if="attachment.thumb_url"
+ class="image"
+ >
+ <img :src="attachment.thumb_url">
</div>
<div class="text">
- <h1><a :href="attachment.url">{{attachment.oembed.title}}</a></h1>
- <div v-html="attachment.oembed.oembedHTML"></div>
+ <!-- eslint-disable vue/no-v-html -->
+ <h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
+ <div v-html="attachment.oembed.oembedHTML" />
+ <!-- eslint-enable vue/no-v-html -->
</div>
</div>
</div>
@@ -68,6 +120,7 @@
max-height: 200px;
max-width: 100%;
display: flex;
+ align-items: center;
video {
max-width: 100%;
}
diff --git a/src/components/autosuggest/autosuggest.js b/src/components/autosuggest/autosuggest.js
index d4efe912..f58f17bb 100644
--- a/src/components/autosuggest/autosuggest.js
+++ b/src/components/autosuggest/autosuggest.js
@@ -2,11 +2,11 @@ const debounceMilliseconds = 500
export default {
props: {
- query: { // function to query results and return a promise
+ query: { // function to query results and return a promise
type: Function,
required: true
},
- filter: { // function to filter results in real time
+ filter: { // function to filter results in real time
type: Function
},
placeholder: {
diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue
index 91657a2d..1f86e996 100644
--- a/src/components/autosuggest/autosuggest.vue
+++ b/src/components/autosuggest/autosuggest.vue
@@ -1,8 +1,22 @@
<template>
- <div class="autosuggest" v-click-outside="onClickOutside">
- <input v-model="term" :placeholder="placeholder" @click="onInputClick" class="autosuggest-input" />
- <div class="autosuggest-results" v-if="resultsVisible && filtered.length > 0">
- <slot v-for="item in filtered" :item="item" />
+ <div
+ v-click-outside="onClickOutside"
+ class="autosuggest"
+ >
+ <input
+ v-model="term"
+ :placeholder="placeholder"
+ class="autosuggest-input"
+ @click="onInputClick"
+ >
+ <div
+ v-if="resultsVisible && filtered.length > 0"
+ class="autosuggest-results"
+ >
+ <slot
+ v-for="item in filtered"
+ :item="item"
+ />
</div>
</div>
</template>
diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue
index c0238570..e1b6e971 100644
--- a/src/components/avatar_list/avatar_list.vue
+++ b/src/components/avatar_list/avatar_list.vue
@@ -1,7 +1,15 @@
<template>
<div class="avatars">
- <router-link :to="userProfileLink(user)" class="avatars-item" v-for="user in slicedUsers">
- <UserAvatar :user="user" class="avatar-small" />
+ <router-link
+ v-for="user in slicedUsers"
+ :key="user.id"
+ :to="userProfileLink(user)"
+ class="avatars-item"
+ >
+ <UserAvatar
+ :user="user"
+ class="avatar-small"
+ />
</router-link>
</div>
</template>
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 634d62b3..8a02174e 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -7,20 +7,45 @@
@click.prevent.native="toggleUserExpanded"
/>
</router-link>
- <div class="basic-user-card-expanded-content" v-if="userExpanded">
- <UserCard :user="user" :rounded="true" :bordered="true"/>
+ <div
+ v-if="userExpanded"
+ class="basic-user-card-expanded-content"
+ >
+ <UserCard
+ :user="user"
+ :rounded="true"
+ :bordered="true"
+ />
</div>
- <div class="basic-user-card-collapsed-content" v-else>
- <div :title="user.name" class="basic-user-card-user-name">
- <span v-if="user.name_html" class="basic-user-card-user-name-value" v-html="user.name_html"></span>
- <span v-else class="basic-user-card-user-name-value">{{ user.name }}</span>
+ <div
+ v-else
+ class="basic-user-card-collapsed-content"
+ >
+ <div
+ :title="user.name"
+ class="basic-user-card-user-name"
+ >
+ <!-- eslint-disable vue/no-v-html -->
+ <span
+ v-if="user.name_html"
+ class="basic-user-card-user-name-value"
+ v-html="user.name_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <span
+ v-else
+ class="basic-user-card-user-name-value"
+ >{{ user.name }}</span>
</div>
<div>
- <router-link class="basic-user-card-screen-name" :to="userProfileLink(user)">
- @{{user.screen_name}}
+ <router-link
+ class="basic-user-card-screen-name"
+ :to="userProfileLink(user)"
+ >
+ @{{ user.screen_name }}
</router-link>
</div>
- <slot></slot>
+ <slot />
</div>
</div>
</template>
@@ -62,6 +87,7 @@
&-expanded-content {
flex: 1;
margin-left: 0.7em;
+ min-width: 0;
}
}
</style>
diff --git a/src/components/block_card/block_card.vue b/src/components/block_card/block_card.vue
index 8eb56e25..5b00b738 100644
--- a/src/components/block_card/block_card.vue
+++ b/src/components/block_card/block_card.vue
@@ -1,7 +1,12 @@
<template>
<basic-user-card :user="user">
<div class="block-card-content-container">
- <button class="btn btn-default" @click="unblockUser" :disabled="progress" v-if="blocked">
+ <button
+ v-if="blocked"
+ class="btn btn-default"
+ :disabled="progress"
+ @click="unblockUser"
+ >
<template v-if="progress">
{{ $t('user_card.unblock_progress') }}
</template>
@@ -9,7 +14,12 @@
{{ $t('user_card.unblock') }}
</template>
</button>
- <button class="btn btn-default" @click="blockUser" :disabled="progress" v-else>
+ <button
+ v-else
+ class="btn btn-default"
+ :disabled="progress"
+ @click="blockUser"
+ >
<template v-if="progress">
{{ $t('user_card.block_progress') }}
</template>
diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js
index bbc9b49f..f2e3adf0 100644
--- a/src/components/chat_panel/chat_panel.js
+++ b/src/components/chat_panel/chat_panel.js
@@ -16,7 +16,7 @@ const chatPanel = {
},
methods: {
submit (message) {
- this.$store.state.chat.channel.push('new_msg', {text: message}, 10000)
+ this.$store.state.chat.channel.push('new_msg', { text: message }, 10000)
this.currentMessage = ''
},
togglePanel () {
diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue
index b37469ac..3677722f 100644
--- a/src/components/chat_panel/chat_panel.vue
+++ b/src/components/chat_panel/chat_panel.vue
@@ -1,41 +1,70 @@
<template>
- <div class="chat-panel" v-if="!this.collapsed || !this.floating">
+ <div
+ v-if="!collapsed || !floating"
+ class="chat-panel"
+ >
<div class="panel panel-default">
- <div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
+ <div
+ class="panel-heading timeline-heading"
+ :class="{ 'chat-heading': floating }"
+ @click.stop.prevent="togglePanel"
+ >
<div class="title">
- <span>{{$t('chat.title')}}</span>
- <i class="icon-cancel" v-if="floating"></i>
+ <span>{{ $t('chat.title') }}</span>
+ <i
+ v-if="floating"
+ class="icon-cancel"
+ />
</div>
</div>
- <div class="chat-window" v-chat-scroll>
- <div class="chat-message" v-for="message in messages" :key="message.id">
+ <div
+ v-chat-scroll
+ class="chat-window"
+ >
+ <div
+ v-for="message in messages"
+ :key="message.id"
+ class="chat-message"
+ >
<span class="chat-avatar">
- <img :src="message.author.avatar" />
+ <img :src="message.author.avatar">
</span>
<div class="chat-content">
<router-link
class="chat-name"
- :to="userProfileLink(message.author)">
- {{message.author.username}}
+ :to="userProfileLink(message.author)"
+ >
+ {{ message.author.username }}
</router-link>
<br>
<span class="chat-text">
- {{message.text}}
+ {{ message.text }}
</span>
</div>
</div>
</div>
<div class="chat-input">
- <textarea @keyup.enter="submit(currentMessage)" v-model="currentMessage" class="chat-input-textarea" rows="1"></textarea>
+ <textarea
+ v-model="currentMessage"
+ class="chat-input-textarea"
+ rows="1"
+ @keyup.enter="submit(currentMessage)"
+ />
</div>
</div>
</div>
- <div v-else class="chat-panel">
+ <div
+ v-else
+ class="chat-panel"
+ >
<div class="panel panel-default">
- <div class="panel-heading stub timeline-heading chat-heading" @click.stop.prevent="togglePanel">
+ <div
+ class="panel-heading stub timeline-heading chat-heading"
+ @click.stop.prevent="togglePanel"
+ >
<div class="title">
- <i class="icon-comment-empty"></i>
- {{$t('chat.title')}}
+ <i class="icon-comment-empty" />
+ {{ $t('chat.title') }}
</div>
</div>
</div>
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 4152b049..2b822ec3 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -1,8 +1,13 @@
<template>
<label class="checkbox">
- <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" :indeterminate.prop="indeterminate">
+ <input
+ type="checkbox"
+ :checked="checked"
+ :indeterminate.prop="indeterminate"
+ @change="$emit('change', $event.target.checked)"
+ >
<i class="checkbox-indicator" />
- <span v-if="!!$slots.default"><slot></slot></span>
+ <span v-if="!!$slots.default"><slot /></span>
</label>
</template>
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 34eec248..9db62e81 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -1,33 +1,44 @@
<template>
-<div class="color-control style-control" :class="{ disabled: !present || disabled }">
- <label :for="name" class="label">
- {{label}}
- </label>
- <input
- v-if="typeof fallback !== 'undefined'"
- class="opt exlcude-disabled"
- :id="name + '-o'"
- type="checkbox"
- :checked="present"
- @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
- <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
- <input
- :id="name"
- class="color-input"
- type="color"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
+ <div
+ class="color-control style-control"
+ :class="{ disabled: !present || disabled }"
+ >
+ <label
+ :for="name"
+ class="label"
>
- <input
- :id="name + '-t'"
- class="text-input"
- type="text"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
+ {{ label }}
+ </label>
+ <input
+ v-if="typeof fallback !== 'undefined'"
+ :id="name + '-o'"
+ class="opt exlcude-disabled"
+ type="checkbox"
+ :checked="present"
+ @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
>
-</div>
+ <label
+ v-if="typeof fallback !== 'undefined'"
+ class="opt-l"
+ :for="name + '-o'"
+ />
+ <input
+ :id="name"
+ class="color-input"
+ type="color"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ @input="$emit('input', $event.target.value)"
+ >
+ <input
+ :id="name + '-t'"
+ class="text-input"
+ type="text"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ @input="$emit('input', $event.target.value)"
+ >
+ </div>
</template>
<script>
diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue
index bd971d00..15a450a2 100644
--- a/src/components/contrast_ratio/contrast_ratio.vue
+++ b/src/components/contrast_ratio/contrast_ratio.vue
@@ -1,28 +1,38 @@
<template>
-<span v-if="contrast" class="contrast-ratio">
- <span :title="hint" class="rating">
- <span v-if="contrast.aaa">
- <i class="icon-thumbs-up-alt"/>
+ <span
+ v-if="contrast"
+ class="contrast-ratio"
+ >
+ <span
+ :title="hint"
+ class="rating"
+ >
+ <span v-if="contrast.aaa">
+ <i class="icon-thumbs-up-alt" />
+ </span>
+ <span v-if="!contrast.aaa && contrast.aa">
+ <i class="icon-adjust" />
+ </span>
+ <span v-if="!contrast.aaa && !contrast.aa">
+ <i class="icon-attention" />
+ </span>
</span>
- <span v-if="!contrast.aaa && contrast.aa">
- <i class="icon-adjust"/>
- </span>
- <span v-if="!contrast.aaa && !contrast.aa">
- <i class="icon-attention"/>
- </span>
- </span>
- <span class="rating" v-if="contrast && large" :title="hint_18pt">
- <span v-if="contrast.laaa">
- <i class="icon-thumbs-up-alt"/>
- </span>
- <span v-if="!contrast.laaa && contrast.laa">
- <i class="icon-adjust"/>
- </span>
- <span v-if="!contrast.laaa && !contrast.laa">
- <i class="icon-attention"/>
+ <span
+ v-if="contrast && large"
+ class="rating"
+ :title="hint_18pt"
+ >
+ <span v-if="contrast.laaa">
+ <i class="icon-thumbs-up-alt" />
+ </span>
+ <span v-if="!contrast.laaa && contrast.laa">
+ <i class="icon-adjust" />
+ </span>
+ <span v-if="!contrast.laaa && !contrast.laa">
+ <i class="icon-attention" />
+ </span>
</span>
</span>
-</span>
</template>
<script>
diff --git a/src/components/conversation-page/conversation-page.vue b/src/components/conversation-page/conversation-page.vue
index 9e322cf5..532f785c 100644
--- a/src/components/conversation-page/conversation-page.vue
+++ b/src/components/conversation-page/conversation-page.vue
@@ -1,9 +1,9 @@
<template>
<conversation
:collapsable="false"
- isPage="true"
+ is-page="true"
:statusoid="statusoid"
- ></conversation>
+ />
</template>
<script src="./conversation-page.js"></script>
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index b3074590..49fa8612 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -42,7 +42,7 @@ const conversation = {
'statusoid',
'collapsable',
'isPage',
- 'showPinned'
+ 'pinnedStatusIdsObject'
],
created () {
if (this.isPage) {
@@ -86,7 +86,8 @@ const conversation = {
},
replies () {
let i = 1
- return reduce(this.conversation, (result, {id, in_reply_to_status_id}) => {
+ // eslint-disable-next-line camelcase
+ return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
/* eslint-disable camelcase */
const irid = in_reply_to_status_id
/* eslint-enable camelcase */
@@ -109,7 +110,7 @@ const conversation = {
Status
},
watch: {
- '$route': 'fetchConversation',
+ status: 'fetchConversation',
expanded (value) {
if (value) {
this.fetchConversation()
@@ -119,15 +120,15 @@ const conversation = {
methods: {
fetchConversation () {
if (this.status) {
- this.$store.state.api.backendInteractor.fetchConversation({id: this.status.id})
- .then(({ancestors, descendants}) => {
+ 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 })
})
.then(() => this.setHighlight(this.statusId))
} else {
const id = this.$route.params.id
- this.$store.state.api.backendInteractor.fetchStatus({id})
+ this.$store.state.api.backendInteractor.fetchStatus({ id })
.then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
.then(() => this.fetchConversation())
}
@@ -139,6 +140,7 @@ const conversation = {
return (this.isExpanded) && id === this.status.id
},
setHighlight (id) {
+ if (!id) return
this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
},
@@ -147,9 +149,6 @@ const conversation = {
},
toggleExpanded () {
this.expanded = !this.expanded
- if (!this.expanded) {
- this.setHighlight(null)
- }
}
}
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 0b4998c3..f184c071 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,25 +1,34 @@
<template>
- <div class="timeline panel-default" :class="[isExpanded ? 'panel' : 'panel-disabled']">
- <div v-if="isExpanded" class="panel-heading conversation-heading">
+ <div
+ class="timeline panel-default"
+ :class="[isExpanded ? 'panel' : 'panel-disabled']"
+ >
+ <div
+ v-if="isExpanded"
+ class="panel-heading conversation-heading"
+ >
<span class="title"> {{ $t('timeline.conversation') }} </span>
<span v-if="collapsable">
- <a href="#" @click.prevent="toggleExpanded">{{ $t('timeline.collapse') }}</a>
+ <a
+ href="#"
+ @click.prevent="toggleExpanded"
+ >{{ $t('timeline.collapse') }}</a>
</span>
</div>
<status
v-for="status in conversation"
- @goto="setHighlight"
- @toggleExpanded="toggleExpanded"
:key="status.id"
- :inlineExpanded="collapsable && isExpanded"
+ :inline-expanded="collapsable && isExpanded"
:statusoid="status"
- :expandable='!isExpanded'
- :showPinned="showPinned"
+ :expandable="!isExpanded"
+ :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:focused="focused(status.id)"
- :inConversation="isExpanded"
+ :in-conversation="isExpanded"
:highlight="getHighlight()"
:replies="getReplies(status.id)"
class="status-fadein panel-body"
+ @goto="setHighlight"
+ @toggleExpanded="toggleExpanded"
/>
</div>
</template>
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
index 3da543de..55d7a7d2 100644
--- a/src/components/dialog_modal/dialog_modal.vue
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -1,16 +1,22 @@
<template>
- <span v-bind:class="{ 'dark-overlay': darkOverlay }" @click.self.stop='onCancel()'>
- <div class="dialog-modal panel panel-default" @click.stop=''>
+ <span
+ :class="{ 'dark-overlay': darkOverlay }"
+ @click.self.stop="onCancel()"
+ >
+ <div
+ class="dialog-modal panel panel-default"
+ @click.stop=""
+ >
<div class="panel-heading dialog-modal-heading">
<div class="title">
- <slot name="header"></slot>
+ <slot name="header" />
</div>
</div>
<div class="dialog-modal-content">
- <slot name="default"></slot>
+ <slot name="default" />
</div>
<div class="dialog-modal-footer user-interactions panel-footer">
- <slot name="footer"></slot>
+ <slot name="footer" />
</div>
</div>
</span>
diff --git a/src/components/dm_timeline/dm_timeline.vue b/src/components/dm_timeline/dm_timeline.vue
index f03da4d3..c4e4d070 100644
--- a/src/components/dm_timeline/dm_timeline.vue
+++ b/src/components/dm_timeline/dm_timeline.vue
@@ -1,5 +1,9 @@
<template>
- <Timeline :title="$t('nav.dms')" v-bind:timeline="timeline" v-bind:timeline-name="'dms'"/>
+ <Timeline
+ :title="$t('nav.dms')"
+ :timeline="timeline"
+ :timeline-name="'dms'"
+ />
</template>
<script src="./dm_timeline.js"></script>
diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji-input/emoji-input.js
index a5bb6eaf..fab64a69 100644
--- a/src/components/emoji-input/emoji-input.js
+++ b/src/components/emoji-input/emoji-input.js
@@ -1,51 +1,122 @@
import Completion from '../../services/completion/completion.js'
-import { take, filter, map } from 'lodash'
+import { take } from 'lodash'
+
+/**
+ * EmojiInput - augmented inputs for emoji and autocomplete support in inputs
+ * without having to give up the comfort of <input/> and <textarea/> elements
+ *
+ * Intended usage is:
+ * <EmojiInput v-model="something">
+ * <input v-model="something"/>
+ * </EmojiInput>
+ *
+ * Works only with <input> and <textarea>. Intended to use with only one nested
+ * input. It will find first input or textarea and work with that, multiple
+ * nested children not tested. You HAVE TO duplicate v-model for both
+ * <emoji-input> and <input>/<textarea> otherwise it will not work.
+ *
+ * Be prepared for CSS troubles though because it still wraps component in a div
+ * while TRYING to make it look like nothing happened, but it could break stuff.
+ */
const EmojiInput = {
- props: [
- 'value',
- 'placeholder',
- 'type',
- 'classname'
- ],
+ props: {
+ suggest: {
+ /**
+ * suggest: function (input: String) => Suggestion[]
+ *
+ * Function that takes input string which takes string (textAtCaret)
+ * and returns an array of Suggestions
+ *
+ * Suggestion is an object containing following properties:
+ * displayText: string. Main display text, what actual suggestion
+ * represents (user's screen name/emoji shortcode)
+ * replacement: string. Text that should replace the textAtCaret
+ * detailText: string, optional. Subtitle text, providing additional info
+ * if present (user's nickname)
+ * imageUrl: string, optional. Image to display alongside with suggestion,
+ * currently if no image is provided, replacement will be used (for
+ * unicode emojis)
+ *
+ * TODO: make it asynchronous when adding proper server-provided user
+ * suggestions
+ *
+ * For commonly used suggestors (emoji, users, both) use suggestor.js
+ */
+ required: true,
+ type: Function
+ },
+ value: {
+ /**
+ * Used for v-model
+ */
+ required: true,
+ type: String
+ }
+ },
data () {
return {
+ input: undefined,
highlighted: 0,
- caret: 0
+ caret: 0,
+ focused: false,
+ blurTimeout: null
}
},
computed: {
suggestions () {
const firstchar = this.textAtCaret.charAt(0)
- if (firstchar === ':') {
- if (this.textAtCaret === ':') { return }
- const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
- if (matchedEmoji.length <= 0) {
- return false
- }
- return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
- shortcode: `:${shortcode}:`,
- utf: utf || '',
+ if (this.textAtCaret === firstchar) { return [] }
+ const matchedSuggestions = this.suggest(this.textAtCaret)
+ if (matchedSuggestions.length <= 0) {
+ return []
+ }
+ return take(matchedSuggestions, 5)
+ .map(({ imageUrl, ...rest }, index) => ({
+ ...rest,
// eslint-disable-next-line camelcase
- img: utf ? '' : this.$store.state.instance.server + image_url,
+ img: imageUrl || '',
highlighted: index === this.highlighted
}))
- } else {
- return false
- }
+ },
+ showPopup () {
+ return this.focused && this.suggestions && this.suggestions.length > 0
},
textAtCaret () {
return (this.wordAtCaret || {}).word || ''
},
wordAtCaret () {
- const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
- return word
- },
- emoji () {
- return this.$store.state.instance.emoji || []
- },
- customEmoji () {
- return this.$store.state.instance.customEmoji || []
+ if (this.value && this.caret) {
+ const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
+ return word
+ }
+ }
+ },
+ mounted () {
+ const slots = this.$slots.default
+ if (!slots || slots.length === 0) return
+ const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag))
+ if (!input) return
+ this.input = input
+ this.resize()
+ input.elm.addEventListener('blur', this.onBlur)
+ input.elm.addEventListener('focus', this.onFocus)
+ input.elm.addEventListener('paste', this.onPaste)
+ input.elm.addEventListener('keyup', this.onKeyUp)
+ input.elm.addEventListener('keydown', this.onKeyDown)
+ input.elm.addEventListener('transitionend', this.onTransition)
+ input.elm.addEventListener('compositionupdate', this.onCompositionUpdate)
+ },
+ unmounted () {
+ const { input } = this
+ if (input) {
+ input.elm.removeEventListener('blur', this.onBlur)
+ input.elm.removeEventListener('focus', this.onFocus)
+ input.elm.removeEventListener('paste', this.onPaste)
+ input.elm.removeEventListener('keyup', this.onKeyUp)
+ input.elm.removeEventListener('keydown', this.onKeyDown)
+ input.elm.removeEventListener('transitionend', this.onTransition)
+ input.elm.removeEventListener('compositionupdate', this.onCompositionUpdate)
}
},
methods: {
@@ -54,27 +125,35 @@ const EmojiInput = {
this.$emit('input', newValue)
this.caret = 0
},
- replaceEmoji (e) {
+ replaceText (e, suggestion) {
const len = this.suggestions.length || 0
- if (this.textAtCaret === ':' || e.ctrlKey) { return }
- if (len > 0) {
- e.preventDefault()
- const emoji = this.suggestions[this.highlighted]
- const replacement = emoji.utf || (emoji.shortcode + ' ')
+ if (this.textAtCaret.length === 1) { return }
+ if (len > 0 || suggestion) {
+ const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
+ const replacement = chosenSuggestion.replacement
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
this.$emit('input', newValue)
- this.caret = 0
this.highlighted = 0
+ const position = this.wordAtCaret.start + replacement.length
+
+ this.$nextTick(function () {
+ // Re-focus inputbox after clicking suggestion
+ this.input.elm.focus()
+ // Set selection right after the replacement instead of the very end
+ this.input.elm.setSelectionRange(position, position)
+ this.caret = position
+ })
+ e.preventDefault()
}
},
cycleBackward (e) {
const len = this.suggestions.length || 0
if (len > 0) {
- e.preventDefault()
this.highlighted -= 1
if (this.highlighted < 0) {
this.highlighted = this.suggestions.length - 1
}
+ e.preventDefault()
} else {
this.highlighted = 0
}
@@ -82,24 +161,88 @@ const EmojiInput = {
cycleForward (e) {
const len = this.suggestions.length || 0
if (len > 0) {
- if (e.shiftKey) { return }
- e.preventDefault()
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = 0
}
+ e.preventDefault()
} else {
this.highlighted = 0
}
},
- onKeydown (e) {
- e.stopPropagation()
+ onTransition (e) {
+ this.resize()
+ },
+ onBlur (e) {
+ // Clicking on any suggestion removes focus from autocomplete,
+ // preventing click handler ever executing.
+ this.blurTimeout = setTimeout(() => {
+ this.focused = false
+ this.setCaret(e)
+ this.resize()
+ }, 200)
+ },
+ onClick (e, suggestion) {
+ this.replaceText(e, suggestion)
+ },
+ onFocus (e) {
+ if (this.blurTimeout) {
+ clearTimeout(this.blurTimeout)
+ this.blurTimeout = null
+ }
+
+ this.focused = true
+ this.setCaret(e)
+ this.resize()
+ },
+ onKeyUp (e) {
+ this.setCaret(e)
+ this.resize()
+ },
+ onPaste (e) {
+ this.setCaret(e)
+ this.resize()
+ },
+ onKeyDown (e) {
+ this.setCaret(e)
+ this.resize()
+
+ const { ctrlKey, shiftKey, key } = e
+ if (key === 'Tab') {
+ if (shiftKey) {
+ this.cycleBackward(e)
+ } else {
+ this.cycleForward(e)
+ }
+ }
+ if (key === 'ArrowUp') {
+ this.cycleBackward(e)
+ } else if (key === 'ArrowDown') {
+ this.cycleForward(e)
+ }
+ if (key === 'Enter') {
+ if (!ctrlKey) {
+ this.replaceText(e)
+ }
+ }
},
onInput (e) {
+ this.setCaret(e)
this.$emit('input', e.target.value)
},
- setCaret ({target: {selectionStart}}) {
+ onCompositionUpdate (e) {
+ this.setCaret(e)
+ this.resize()
+ this.$emit('input', e.target.value)
+ },
+ setCaret ({ target: { selectionStart } }) {
this.caret = selectionStart
+ },
+ resize () {
+ const { panel } = this.$refs
+ if (!panel) return
+ const { offsetHeight, offsetTop } = this.input.elm
+ this.$refs.panel.style.top = (offsetTop + offsetHeight) + 'px'
}
}
}
diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue
index 338b77cd..48739ec8 100644
--- a/src/components/emoji-input/emoji-input.vue
+++ b/src/components/emoji-input/emoji-input.vue
@@ -1,50 +1,30 @@
<template>
<div class="emoji-input">
- <input
- v-if="type !== 'textarea'"
- :class="classname"
- :type="type"
- :value="value"
- :placeholder="placeholder"
- @input="onInput"
- @click="setCaret"
- @keyup="setCaret"
- @keydown="onKeydown"
- @keydown.down="cycleForward"
- @keydown.up="cycleBackward"
- @keydown.shift.tab="cycleBackward"
- @keydown.tab="cycleForward"
- @keydown.enter="replaceEmoji"
- />
- <textarea
- v-else
- :class="classname"
- :value="value"
- :placeholder="placeholder"
- @input="onInput"
- @click="setCaret"
- @keyup="setCaret"
- @keydown="onKeydown"
- @keydown.down="cycleForward"
- @keydown.up="cycleBackward"
- @keydown.shift.tab="cycleBackward"
- @keydown.tab="cycleForward"
- @keydown.enter="replaceEmoji"
- ></textarea>
- <div class="autocomplete-panel" v-if="suggestions">
+ <slot />
+ <div
+ ref="panel"
+ class="autocomplete-panel"
+ :class="{ hide: !showPopup }"
+ >
<div class="autocomplete-panel-body">
<div
- v-for="(emoji, index) in suggestions"
+ v-for="(suggestion, index) in suggestions"
:key="index"
- @click="replace(emoji.utf || (emoji.shortcode + ' '))"
class="autocomplete-item"
- :class="{ highlighted: emoji.highlighted }"
+ :class="{ highlighted: suggestion.highlighted }"
+ @click.stop.prevent="onClick($event, suggestion)"
>
- <span v-if="emoji.img">
- <img :src="emoji.img" />
+ <span class="image">
+ <img
+ v-if="suggestion.img"
+ :src="suggestion.img"
+ >
+ <span v-else>{{ suggestion.replacement }}</span>
</span>
- <span v-else>{{emoji.utf}}</span>
- <span>{{emoji.shortcode}}</span>
+ <div class="label">
+ <span class="displayText">{{ suggestion.displayText }}</span>
+ <span class="detailText">{{ suggestion.detailText }}</span>
+ </div>
</div>
</div>
</div>
@@ -57,8 +37,81 @@
@import '../../_variables.scss';
.emoji-input {
- .form-control {
- width: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .autocomplete {
+ &-panel {
+ position: absolute;
+ z-index: 9;
+ margin-top: 2px;
+
+ &.hide {
+ display: none
+ }
+
+ &-body {
+ margin: 0 0.5em 0 0.5em;
+ border-radius: $fallback--tooltipRadius;
+ border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
+ box-shadow: var(--popupShadow);
+ min-width: 75%;
+ background: $fallback--bg;
+ background: var(--bg, $fallback--bg);
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ }
+
+ &-item {
+ display: flex;
+ cursor: pointer;
+ padding: 0.2em 0.4em;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.4);
+ height: 32px;
+
+ .image {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ text-align: center;
+ font-size: 32px;
+
+ margin-right: 4px;
+
+ img {
+ width: 32px;
+ height: 32px;
+ object-fit: contain;
+ }
+ }
+
+ .label {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0.1em 0 0.2em;
+
+ .displayText {
+ line-height: 1.5;
+ }
+
+ .detailText {
+ font-size: 9px;
+ line-height: 9px;
+ }
+ }
+
+ &.highlighted {
+ background-color: $fallback--fg;
+ background-color: var(--lightBg, $fallback--fg);
+ }
+ }
+ }
+
+ input, textarea {
+ flex: 1 0 auto;
}
}
</style>
diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji-input/suggestor.js
new file mode 100644
index 00000000..aec5c39d
--- /dev/null
+++ b/src/components/emoji-input/suggestor.js
@@ -0,0 +1,94 @@
+import { debounce } from 'lodash'
+/**
+ * suggest - generates a suggestor function to be used by emoji-input
+ * data: object providing source information for specific types of suggestions:
+ * data.emoji - optional, an array of all emoji available i.e.
+ * (state.instance.emoji + state.instance.customEmoji)
+ * data.users - optional, an array of all known users
+ * updateUsersList - optional, a function to search and append to users
+ *
+ * Depending on data present one or both (or none) can be present, so if field
+ * doesn't support user linking you can just provide only emoji.
+ */
+
+const debounceUserSearch = debounce((data, input) => {
+ data.updateUsersList(input)
+}, 500, { leading: true, trailing: false })
+
+export default data => input => {
+ const firstChar = input[0]
+ if (firstChar === ':' && data.emoji) {
+ return suggestEmoji(data.emoji)(input)
+ }
+ if (firstChar === '@' && data.users) {
+ return suggestUsers(data)(input)
+ }
+ return []
+}
+
+export const suggestEmoji = emojis => input => {
+ const noPrefix = input.toLowerCase().substr(1)
+ return emojis
+ .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
+ .sort((a, b) => {
+ let aScore = 0
+ let bScore = 0
+
+ // Make custom emojis a priority
+ aScore += a.imageUrl ? 10 : 0
+ bScore += b.imageUrl ? 10 : 0
+
+ // Sort alphabetically
+ const alphabetically = a.displayText > b.displayText ? 1 : -1
+
+ return bScore - aScore + alphabetically
+ })
+}
+
+export const suggestUsers = data => input => {
+ const noPrefix = input.toLowerCase().substr(1)
+ const users = data.users
+
+ const newUsers = users.filter(
+ user =>
+ user.screen_name.toLowerCase().startsWith(noPrefix) ||
+ user.name.toLowerCase().startsWith(noPrefix)
+
+ /* taking only 20 results so that sorting is a bit cheaper, we display
+ * only 5 anyway. could be inaccurate, but we ideally we should query
+ * backend anyway
+ */
+ ).slice(0, 20).sort((a, b) => {
+ let aScore = 0
+ let bScore = 0
+
+ // Matches on screen name (i.e. user@instance) makes a priority
+ aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
+ bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
+
+ // Matches on name takes second priority
+ aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
+ bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
+
+ const diff = (bScore - aScore) * 10
+
+ // Then sort alphabetically
+ const nameAlphabetically = a.name > b.name ? 1 : -1
+ const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
+
+ return diff + nameAlphabetically + screenNameAlphabetically
+ /* eslint-disable camelcase */
+ }).map(({ screen_name, name, profile_image_url_original }) => ({
+ displayText: screen_name,
+ detailText: name,
+ imageUrl: profile_image_url_original,
+ replacement: '@' + screen_name + ' '
+ }))
+
+ // BE search users if there are no matches
+ if (newUsers.length === 0 && data.updateUsersList) {
+ debounceUserSearch(data, noPrefix)
+ }
+ return newUsers
+ /* eslint-enable camelcase */
+}
diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue
index 451a2668..20c6f569 100644
--- a/src/components/export_import/export_import.vue
+++ b/src/components/export_import/export_import.vue
@@ -1,12 +1,27 @@
<template>
-<div class="import-export-container">
- <slot name="before"/>
- <button class="btn" @click="exportData">{{ exportLabel }}</button>
- <button class="btn" @click="importData">{{ importLabel }}</button>
- <slot name="afterButtons"/>
- <p v-if="importFailed" class="alert error">{{ importFailedText }}</p>
- <slot name="afterError"/>
-</div>
+ <div class="import-export-container">
+ <slot name="before" />
+ <button
+ class="btn"
+ @click="exportData"
+ >
+ {{ exportLabel }}
+ </button>
+ <button
+ class="btn"
+ @click="importData"
+ >
+ {{ importLabel }}
+ </button>
+ <slot name="afterButtons" />
+ <p
+ v-if="importFailed"
+ class="alert error"
+ >
+ {{ importFailedText }}
+ </p>
+ <slot name="afterError" />
+ </div>
</template>
<script>
@@ -49,7 +64,7 @@ export default {
if (event.target.files[0]) {
// eslint-disable-next-line no-undef
const reader = new FileReader()
- reader.onload = ({target}) => {
+ reader.onload = ({ target }) => {
try {
const parsed = JSON.parse(target.result)
const valid = this.validator(parsed)
diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue
index f22e579e..f5126dc1 100644
--- a/src/components/exporter/exporter.vue
+++ b/src/components/exporter/exporter.vue
@@ -1,10 +1,16 @@
<template>
<div class="exporter">
<div v-if="processing">
- <i class="icon-spin4 animate-spin exporter-processing"></i>
- <span>{{processingMessage}}</span>
+ <i class="icon-spin4 animate-spin exporter-processing" />
+ <span>{{ processingMessage }}</span>
</div>
- <button class="btn btn-default" @click="process" v-else>{{exportButtonLabel}}</button>
+ <button
+ v-else
+ class="btn btn-default"
+ @click="process"
+ >
+ {{ exportButtonLabel }}
+ </button>
</div>
</template>
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 528da301..5ac73e97 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,45 +1,31 @@
-import Popper from 'vue-popperjs/src/component/popper.js.vue'
-
const ExtraButtons = {
props: [ 'status' ],
- components: {
- Popper
- },
- data () {
- return {
- showDropDown: false,
- showPopper: true
- }
- },
methods: {
deleteStatus () {
- this.refreshPopper()
const confirmed = window.confirm(this.$t('status.delete_confirm'))
if (confirmed) {
this.$store.dispatch('deleteStatus', { id: this.status.id })
}
},
- toggleMenu () {
- this.showDropDown = !this.showDropDown
- },
pinStatus () {
- this.refreshPopper()
this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
unpinStatus () {
- this.refreshPopper()
this.$store.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
- refreshPopper () {
- this.showPopper = false
- this.showDropDown = false
- setTimeout(() => {
- this.showPopper = true
- })
+ muteConversation () {
+ this.$store.dispatch('muteConversation', this.status.id)
+ .then(() => this.$emit('onSuccess'))
+ .catch(err => this.$emit('onError', err.error.error))
+ },
+ unmuteConversation () {
+ this.$store.dispatch('unmuteConversation', this.status.id)
+ .then(() => this.$emit('onSuccess'))
+ .catch(err => this.$emit('onError', err.error.error))
}
},
computed: {
@@ -55,8 +41,8 @@ const ExtraButtons = {
canPin () {
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
},
- enabled () {
- return this.canPin || this.canDelete
+ canMute () {
+ return !!this.currentUser
}
}
}
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index a761d313..ed0f3aa4 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,34 +1,58 @@
<template>
- <Popper
+ <v-popover
+ v-if="canDelete || canMute || canPin"
trigger="click"
- @hide='showDropDown = false'
- append-to-body
- v-if="enabled && showPopper"
- :options="{
- placement: 'top',
- modifiers: {
- arrow: { enabled: true },
- offset: { offset: '0, 5px' },
- }
- }"
+ placement="top"
+ class="extra-button-popover"
+ :offset="5"
+ :container="false"
>
- <div class="popper-wrapper">
+ <div slot="popover">
<div class="dropdown-menu">
- <button class="dropdown-item dropdown-item-icon" @click.prevent="pinStatus" v-if="!status.pinned && canPin">
- <i class="icon-pin"></i><span>{{$t("status.pin")}}</span>
+ <button
+ v-if="canMute && !status.muted"
+ class="dropdown-item dropdown-item-icon"
+ @click.prevent="muteConversation"
+ >
+ <i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span>
</button>
- <button class="dropdown-item dropdown-item-icon" @click.prevent="unpinStatus" v-if="status.pinned && canPin">
- <i class="icon-pin"></i><span>{{$t("status.unpin")}}</span>
+ <button
+ v-if="canMute && status.muted"
+ class="dropdown-item dropdown-item-icon"
+ @click.prevent="unmuteConversation"
+ >
+ <i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span>
</button>
- <button class="dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" v-if="canDelete">
- <i class="icon-cancel"></i><span>{{$t("status.delete")}}</span>
+ <button
+ v-if="!status.pinned && canPin"
+ v-close-popover
+ class="dropdown-item dropdown-item-icon"
+ @click.prevent="pinStatus"
+ >
+ <i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
+ </button>
+ <button
+ v-if="status.pinned && canPin"
+ v-close-popover
+ class="dropdown-item dropdown-item-icon"
+ @click.prevent="unpinStatus"
+ >
+ <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
+ </button>
+ <button
+ v-if="canDelete"
+ v-close-popover
+ class="dropdown-item dropdown-item-icon"
+ @click.prevent="deleteStatus"
+ >
+ <i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
</button>
</div>
</div>
- <div class="button-icon" slot="reference" @click="toggleMenu">
- <i class='icon-ellipsis' :class="{'icon-clicked': showDropDown}"></i>
+ <div class="button-icon">
+ <i class="icon-ellipsis" />
</div>
- </Popper>
+ </v-popover>
</template>
<script src="./extra_buttons.js" ></script>
@@ -40,7 +64,8 @@
.icon-ellipsis {
cursor: pointer;
- &:hover, &.icon-clicked {
+ &:hover,
+ .extra-button-popover.open & {
color: $fallback--text;
color: var(--text, $fallback--text);
}
diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
index a2b4cb65..a24eacbf 100644
--- a/src/components/favorite_button/favorite_button.js
+++ b/src/components/favorite_button/favorite_button.js
@@ -11,9 +11,9 @@ const FavoriteButton = {
methods: {
favorite () {
if (!this.status.favorited) {
- this.$store.dispatch('favorite', {id: this.status.id})
+ this.$store.dispatch('favorite', { id: this.status.id })
} else {
- this.$store.dispatch('unfavorite', {id: this.status.id})
+ this.$store.dispatch('unfavorite', { id: this.status.id })
}
this.animated = true
setTimeout(() => {
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index 05ce6bd0..06ce9983 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,11 +1,20 @@
<template>
<div v-if="loggedIn">
- <i :class='classes' class='button-icon favorite-button fav-active' @click.prevent='favorite()' :title="$t('tool_tip.favorite')"/>
- <span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span>
+ <i
+ :class="classes"
+ class="button-icon favorite-button fav-active"
+ :title="$t('tool_tip.favorite')"
+ @click.prevent="favorite()"
+ />
+ <span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
<div v-else>
- <i :class='classes' class='button-icon favorite-button' :title="$t('tool_tip.favorite')"/>
- <span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span>
+ <i
+ :class="classes"
+ class="button-icon favorite-button"
+ :title="$t('tool_tip.favorite')"
+ />
+ <span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
</template>
diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js
index 5f0b7b25..5f80a079 100644
--- a/src/components/features_panel/features_panel.js
+++ b/src/components/features_panel/features_panel.js
@@ -1,8 +1,6 @@
const FeaturesPanel = {
computed: {
- chat: function () {
- return this.$store.state.instance.chatAvailable && (!this.$store.state.chatDisabled)
- },
+ chat: function () { return this.$store.state.instance.chatAvailable },
gopher: function () { return this.$store.state.instance.gopherAvailable },
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue
index 7a263e01..3e5939a6 100644
--- a/src/components/features_panel/features_panel.vue
+++ b/src/components/features_panel/features_panel.vue
@@ -3,17 +3,25 @@
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
- {{$t('features_panel.title')}}
+ {{ $t('features_panel.title') }}
</div>
</div>
<div class="panel-body features-panel">
<ul>
- <li v-if="chat">{{$t('features_panel.chat')}}</li>
- <li v-if="gopher">{{$t('features_panel.gopher')}}</li>
- <li v-if="whoToFollow">{{$t('features_panel.who_to_follow')}}</li>
- <li v-if="mediaProxy">{{$t('features_panel.media_proxy')}}</li>
- <li>{{$t('features_panel.scope_options')}}</li>
- <li>{{$t('features_panel.text_limit')}} = {{textlimit}}</li>
+ <li v-if="chat">
+ {{ $t('features_panel.chat') }}
+ </li>
+ <li v-if="gopher">
+ {{ $t('features_panel.gopher') }}
+ </li>
+ <li v-if="whoToFollow">
+ {{ $t('features_panel.who_to_follow') }}
+ </li>
+ <li v-if="mediaProxy">
+ {{ $t('features_panel.media_proxy') }}
+ </li>
+ <li>{{ $t('features_panel.scope_options') }}</li>
+ <li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
</ul>
</div>
</div>
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 94e2836f..310fe843 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -1,11 +1,17 @@
<template>
<basic-user-card :user="user">
<div class="follow-card-content-container">
- <span class="faint" v-if="!noFollowsYou && user.follows_you">
+ <span
+ v-if="!noFollowsYou && user.follows_you"
+ class="faint"
+ >
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<template v-if="!loggedIn">
- <div class="follow-card-follow-button" v-if="!user.following">
+ <div
+ v-if="!user.following"
+ class="follow-card-follow-button"
+ >
<RemoteFollow :user="user" />
</div>
</template>
@@ -13,9 +19,9 @@
<button
v-if="!user.following"
class="btn btn-default follow-card-follow-button"
- @click="followUser"
:disabled="inProgress"
:title="requestSent ? $t('user_card.follow_again') : ''"
+ @click="followUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
@@ -27,7 +33,12 @@
{{ $t('user_card.follow') }}
</template>
</button>
- <button v-else class="btn btn-default follow-card-follow-button pressed" @click="unfollowUser" :disabled="inProgress">
+ <button
+ v-else
+ class="btn btn-default follow-card-follow-button pressed"
+ :disabled="inProgress"
+ @click="unfollowUser"
+ >
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue
index 4a3bbba4..b217b8ed 100644
--- a/src/components/follow_request_card/follow_request_card.vue
+++ b/src/components/follow_request_card/follow_request_card.vue
@@ -1,8 +1,18 @@
<template>
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
- <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
- <button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
+ <button
+ class="btn btn-default"
+ @click="approveUser"
+ >
+ {{ $t('user_card.approve') }}
+ </button>
+ <button
+ class="btn btn-default"
+ @click="denyUser"
+ >
+ {{ $t('user_card.deny') }}
+ </button>
</div>
</basic-user-card>
</template>
diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue
index 36901fb4..5fa4cf39 100644
--- a/src/components/follow_requests/follow_requests.vue
+++ b/src/components/follow_requests/follow_requests.vue
@@ -1,10 +1,15 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
- {{$t('nav.friend_requests')}}
+ {{ $t('nav.friend_requests') }}
</div>
<div class="panel-body">
- <FollowRequestCard v-for="request in requests" :key="request.id" :user="request" class="list-item"/>
+ <FollowRequestCard
+ v-for="request in requests"
+ :key="request.id"
+ :user="request"
+ class="list-item"
+ />
</div>
</div>
</template>
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
index ed36b280..61f0384b 100644
--- a/src/components/font_control/font_control.vue
+++ b/src/components/font_control/font_control.vue
@@ -1,35 +1,56 @@
<template>
-<div class="font-control style-control" :class="{ custom: isCustom }">
- <label :for="preset === 'custom' ? name : name + '-font-switcher'" class="label">
- {{label}}
- </label>
- <input
- v-if="typeof fallback !== 'undefined'"
- class="opt exlcude-disabled"
- type="checkbox"
- :id="name + '-o'"
- :checked="present"
- @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)">
- <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
- <label :for="name + '-font-switcher'" class="select" :disabled="!present">
- <select
+ <div
+ class="font-control style-control"
+ :class="{ custom: isCustom }"
+ >
+ <label
+ :for="preset === 'custom' ? name : name + '-font-switcher'"
+ class="label"
+ >
+ {{ label }}
+ </label>
+ <input
+ v-if="typeof fallback !== 'undefined'"
+ :id="name + '-o'"
+ class="opt exlcude-disabled"
+ type="checkbox"
+ :checked="present"
+ @input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
+ >
+ <label
+ v-if="typeof fallback !== 'undefined'"
+ class="opt-l"
+ :for="name + '-o'"
+ />
+ <label
+ :for="name + '-font-switcher'"
+ class="select"
:disabled="!present"
- v-model="preset"
- class="font-switcher"
- :id="name + '-font-switcher'">
- <option v-for="option in availableOptions" :value="option">
- {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
- </option>
- </select>
- <i class="icon-down-open"/>
- </label>
- <input
- v-if="isCustom"
- class="custom-font"
- type="text"
- :id="name"
- v-model="family">
-</div>
+ >
+ <select
+ :id="name + '-font-switcher'"
+ v-model="preset"
+ :disabled="!present"
+ class="font-switcher"
+ >
+ <option
+ v-for="option in availableOptions"
+ :key="option"
+ :value="option"
+ >
+ {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ <input
+ v-if="isCustom"
+ :id="name"
+ v-model="family"
+ class="custom-font"
+ type="text"
+ >
+ </div>
</template>
<script src="./font_control.js" ></script>
diff --git a/src/components/friends_timeline/friends_timeline.vue b/src/components/friends_timeline/friends_timeline.vue
index 66c0c058..01a56812 100644
--- a/src/components/friends_timeline/friends_timeline.vue
+++ b/src/components/friends_timeline/friends_timeline.vue
@@ -1,5 +1,9 @@
<template>
- <Timeline :title="$t('nav.timeline')" v-bind:timeline="timeline" v-bind:timeline-name="'friends'"/>
+ <Timeline
+ :title="$t('nav.timeline')"
+ :timeline="timeline"
+ :timeline-name="'friends'"
+ />
</template>
<script src="./friends_timeline.js"></script>
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index ea525c95..6169d294 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -1,13 +1,22 @@
<template>
- <div ref="galleryContainer" style="width: 100%;">
- <div class="gallery-row" v-for="row in rows" :style="rowHeight(row.length)" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }">
+ <div
+ ref="galleryContainer"
+ style="width: 100%;"
+ >
+ <div
+ v-for="(row, index) in rows"
+ :key="index"
+ class="gallery-row"
+ :style="rowHeight(row.length)"
+ :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
+ >
<attachment
v-for="attachment in row"
- :setMedia="setMedia"
+ :key="attachment.id"
+ :set-media="setMedia"
:nsfw="nsfw"
:attachment="attachment"
- :allowPlay="false"
- :key="attachment.id"
+ :allow-play="false"
/>
</div>
</div>
@@ -28,7 +37,9 @@
flex-grow: 1;
margin-top: 0.5em;
- .attachments, .attachment {
+ // FIXME: specificity problem with this and .attachments.attachment
+ // we shouldn't have the need for .image here
+ .attachment.image {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
@@ -50,13 +61,17 @@
}
&.contain-fit {
- img, video {
+ img,
+ video,
+ canvas {
object-fit: contain;
}
}
&.cover-fit {
- img, video {
+ img,
+ video,
+ canvas {
object-fit: cover;
}
}
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index d2b86e9e..4e1b5927 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -2,20 +2,57 @@
<div class="image-cropper">
<div v-if="dataUrl">
<div class="image-cropper-image-container">
- <img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
+ <img
+ ref="img"
+ :src="dataUrl"
+ alt=""
+ @load.stop="createCropper"
+ >
</div>
<div class="image-cropper-buttons-wrapper">
- <button class="btn" type="button" :disabled="submitting" @click="submit()" v-text="saveText"></button>
- <button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
- <button class="btn" type="button" :disabled="submitting" @click="submit(false)" v-text="saveWithoutCroppingText"></button>
- <i class="icon-spin4 animate-spin" v-if="submitting"></i>
+ <button
+ class="btn"
+ type="button"
+ :disabled="submitting"
+ @click="submit()"
+ v-text="saveText"
+ />
+ <button
+ class="btn"
+ type="button"
+ :disabled="submitting"
+ @click="destroy"
+ v-text="cancelText"
+ />
+ <button
+ class="btn"
+ type="button"
+ :disabled="submitting"
+ @click="submit(false)"
+ v-text="saveWithoutCroppingText"
+ />
+ <i
+ v-if="submitting"
+ class="icon-spin4 animate-spin"
+ />
</div>
- <div class="alert error" v-if="submitError">
- {{submitErrorMsg}}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <div
+ v-if="submitError"
+ class="alert error"
+ >
+ {{ submitErrorMsg }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearError"
+ />
</div>
</div>
- <input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
+ <input
+ ref="input"
+ type="file"
+ class="image-cropper-img-input"
+ :accept="mimes"
+ >
</div>
</template>
diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue
index 0c5aa93d..ed923d59 100644
--- a/src/components/importer/importer.vue
+++ b/src/components/importer/importer.vue
@@ -1,17 +1,36 @@
<template>
<div class="importer">
<form>
- <input type="file" ref="input" v-on:change="change" />
+ <input
+ ref="input"
+ type="file"
+ @change="change"
+ >
</form>
- <i class="icon-spin4 animate-spin importer-uploading" v-if="submitting"></i>
- <button class="btn btn-default" v-else @click="submit">{{submitButtonLabel}}</button>
+ <i
+ v-if="submitting"
+ class="icon-spin4 animate-spin importer-uploading"
+ />
+ <button
+ v-else
+ class="btn btn-default"
+ @click="submit"
+ >
+ {{ submitButtonLabel }}
+ </button>
<div v-if="success">
- <i class="icon-cross" @click="dismiss"></i>
- <p>{{successMessage}}</p>
+ <i
+ class="icon-cross"
+ @click="dismiss"
+ />
+ <p>{{ successMessage }}</p>
</div>
<div v-else-if="error">
- <i class="icon-cross" @click="dismiss"></i>
- <p>{{errorMessage}}</p>
+ <i
+ class="icon-cross"
+ @click="dismiss"
+ />
+ <p>{{ errorMessage }}</p>
</div>
</div>
</template>
diff --git a/src/components/instance_specific_panel/instance_specific_panel.js b/src/components/instance_specific_panel/instance_specific_panel.js
index 9bb5e945..09e3d055 100644
--- a/src/components/instance_specific_panel/instance_specific_panel.js
+++ b/src/components/instance_specific_panel/instance_specific_panel.js
@@ -2,9 +2,6 @@ const InstanceSpecificPanel = {
computed: {
instanceSpecificPanelContent () {
return this.$store.state.instance.instanceSpecificPanelContent
- },
- show () {
- return !this.$store.state.config.hideISP
}
}
}
diff --git a/src/components/instance_specific_panel/instance_specific_panel.vue b/src/components/instance_specific_panel/instance_specific_panel.vue
index a7b74667..7448ca06 100644
--- a/src/components/instance_specific_panel/instance_specific_panel.vue
+++ b/src/components/instance_specific_panel/instance_specific_panel.vue
@@ -1,15 +1,13 @@
<template>
- <div v-if="show" class="instance-specific-panel">
+ <div class="instance-specific-panel">
<div class="panel panel-default">
<div class="panel-body">
- <div v-html="instanceSpecificPanelContent">
- </div>
+ <!-- eslint-disable vue/no-v-html -->
+ <div v-html="instanceSpecificPanelContent" />
+ <!-- eslint-enable vue/no-v-html -->
</div>
</div>
</div>
</template>
<script src="./instance_specific_panel.js" ></script>
-
-<style lang="scss">
-</style>
diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js
index d4e3cc17..1f8a9de9 100644
--- a/src/components/interactions/interactions.js
+++ b/src/components/interactions/interactions.js
@@ -13,8 +13,8 @@ const Interactions = {
}
},
methods: {
- onModeSwitch (index, dataset) {
- this.filterMode = tabModeDict[dataset.filter]
+ onModeSwitch (key) {
+ this.filterMode = tabModeDict[key]
}
},
components: {
diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue
index 38b2670d..08cee343 100644
--- a/src/components/interactions/interactions.vue
+++ b/src/components/interactions/interactions.vue
@@ -7,18 +7,27 @@
</div>
<tab-switcher
ref="tabSwitcher"
- :onSwitch="onModeSwitch"
- >
- <span data-tab-dummy data-filter="mentions" :label="$t('nav.mentions')"/>
- <span data-tab-dummy data-filter="likes+repeats" :label="$t('interactions.favs_repeats')"/>
- <span data-tab-dummy data-filter="follows" :label="$t('interactions.follows')"/>
+ :on-switch="onModeSwitch"
+ >
+ <span
+ key="mentions"
+ :label="$t('nav.mentions')"
+ />
+ <span
+ key="likes+repeats"
+ :label="$t('interactions.favs_repeats')"
+ />
+ <span
+ key="follows"
+ :label="$t('interactions.follows')"
+ />
</tab-switcher>
<Notifications
ref="notifications"
- :noHeading="true"
- :minimalMode="true"
- :filterMode="filterMode"
- />
+ :no-heading="true"
+ :minimal-mode="true"
+ :filter-mode="filterMode"
+ />
</div>
</template>
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 9f7877c6..83df9a0b 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -3,50 +3,60 @@
<label for="interface-language-switcher">
{{ $t('settings.interfaceLanguage') }}
</label>
- <label for="interface-language-switcher" class='select'>
- <select id="interface-language-switcher" v-model="language">
- <option v-for="(langCode, i) in languageCodes" :value="langCode">
+ <label
+ for="interface-language-switcher"
+ class="select"
+ >
+ <select
+ id="interface-language-switcher"
+ v-model="language"
+ >
+ <option
+ v-for="(langCode, i) in languageCodes"
+ :key="langCode"
+ :value="langCode"
+ >
{{ languageNames[i] }}
</option>
</select>
- <i class="icon-down-open"/>
+ <i class="icon-down-open" />
</label>
</div>
</template>
<script>
- import languagesObject from '../../i18n/messages'
- import ISO6391 from 'iso-639-1'
- import _ from 'lodash'
+import languagesObject from '../../i18n/messages'
+import ISO6391 from 'iso-639-1'
+import _ from 'lodash'
- export default {
- computed: {
- languageCodes () {
- return Object.keys(languagesObject)
- },
+export default {
+ computed: {
+ languageCodes () {
+ return Object.keys(languagesObject)
+ },
- languageNames () {
- return _.map(this.languageCodes, this.getLanguageName)
- },
+ languageNames () {
+ return _.map(this.languageCodes, this.getLanguageName)
+ },
- language: {
- get: function () { return this.$store.state.config.interfaceLanguage },
- set: function (val) {
- this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
- this.$i18n.locale = val
- }
+ language: {
+ get: function () { return this.$store.state.config.interfaceLanguage },
+ set: function (val) {
+ this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+ this.$i18n.locale = val
}
- },
+ }
+ },
- methods: {
- getLanguageName (code) {
- const specialLanguageNames = {
- 'ja': 'Japanese (やさしいにほんご)',
- 'ja_pedantic': 'Japanese (日本語)',
- 'zh': 'Chinese (简体中文)'
- }
- return specialLanguageNames[code] || ISO6391.getName(code)
+ methods: {
+ getLanguageName (code) {
+ const specialLanguageNames = {
+ 'ja': 'Japanese (やさしいにほんご)',
+ 'ja_pedantic': 'Japanese (日本語)',
+ 'zh': 'Chinese (简体中文)'
}
+ return specialLanguageNames[code] || ISO6391.getName(code)
}
}
+}
</script>
diff --git a/src/components/link-preview/link-preview.js b/src/components/link-preview/link-preview.js
index 2f6da55e..444aafbe 100644
--- a/src/components/link-preview/link-preview.js
+++ b/src/components/link-preview/link-preview.js
@@ -5,6 +5,11 @@ const LinkPreview = {
'size',
'nsfw'
],
+ data () {
+ return {
+ imageLoaded: false
+ }
+ },
computed: {
useImage () {
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
@@ -15,6 +20,15 @@ const LinkPreview = {
useDescription () {
return this.card.description && /\S/.test(this.card.description)
}
+ },
+ created () {
+ if (this.useImage) {
+ const newImg = new Image()
+ newImg.onload = () => {
+ this.imageLoaded = true
+ }
+ newImg.src = this.card.image
+ }
}
}
diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue
index 64b1a58b..69171977 100644
--- a/src/components/link-preview/link-preview.vue
+++ b/src/components/link-preview/link-preview.vue
@@ -1,13 +1,25 @@
<template>
<div>
- <a class="link-preview-card" :href="card.url" target="_blank" rel="noopener">
- <div class="card-image" :class="{ 'small-image': size === 'small' }" v-if="useImage">
- <img :src="card.image"></img>
+ <a
+ class="link-preview-card"
+ :href="card.url"
+ target="_blank"
+ rel="noopener"
+ >
+ <div
+ v-if="useImage && imageLoaded"
+ class="card-image"
+ :class="{ 'small-image': size === 'small' }"
+ >
+ <img :src="card.image">
</div>
<div class="card-content">
<span class="card-host faint">{{ card.provider_name }}</span>
<h4 class="card-title">{{ card.title }}</h4>
- <p class="card-description" v-if="useDescription">{{ card.description }}</p>
+ <p
+ v-if="useDescription"
+ class="card-description"
+ >{{ card.description }}</p>
</div>
</a>
</div>
diff --git a/src/components/list/list.vue b/src/components/list/list.vue
index 7136915b..a6223cce 100644
--- a/src/components/list/list.vue
+++ b/src/components/list/list.vue
@@ -1,9 +1,19 @@
<template>
<div class="list">
- <div v-for="item in items" class="list-item" :key="getKey(item)">
- <slot name="item" :item="item" />
+ <div
+ v-for="item in items"
+ :key="getKey(item)"
+ class="list-item"
+ >
+ <slot
+ name="item"
+ :item="item"
+ />
</div>
- <div class="list-empty-content faint" v-if="items.length === 0 && !!$slots.empty">
+ <div
+ v-if="items.length === 0 && !!$slots.empty"
+ class="list-empty-content faint"
+ >
<slot name="empty" />
</div>
</div>
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 93214646..10f52fe2 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -26,9 +26,10 @@ const LoginForm = {
this.isTokenAuth ? this.submitToken() : this.submitPassword()
},
submitToken () {
- const { clientId } = this.oauth
+ const { clientId, clientSecret } = this.oauth
const data = {
clientId,
+ clientSecret,
instance: this.instance.server,
commit: this.$store.commit
}
@@ -57,7 +58,7 @@ const LoginForm = {
).then((result) => {
if (result.error) {
if (result.error === 'mfa_required') {
- this.requireMFA({app: app, settings: result})
+ this.requireMFA({ app: app, settings: result })
} else {
this.error = result.error
this.focusOnPasswordInput()
@@ -65,7 +66,7 @@ const LoginForm = {
return
}
this.login(result).then(() => {
- this.$router.push({name: 'friends'})
+ this.$router.push({ name: 'friends' })
})
})
})
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index a2c5cf8f..3ec7fe0c 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -1,53 +1,83 @@
<template>
-<div class="login panel panel-default">
- <!-- Default panel contents -->
-
- <div class="panel-heading">{{$t('login.login')}}</div>
-
- <div class="panel-body">
- <form class='login-form' @submit.prevent='submit'>
- <template v-if="isPasswordAuth">
- <div class='form-group'>
- <label for='username'>{{$t('login.username')}}</label>
- <input :disabled="loggingIn" v-model='user.username'
- class='form-control' id='username'
- :placeholder="$t('login.placeholder')">
- </div>
- <div class='form-group'>
- <label for='password'>{{$t('login.password')}}</label>
- <input :disabled="loggingIn" v-model='user.password'
- ref='passwordInput' class='form-control' id='password' type='password'>
- </div>
- </template>
+ <div class="login panel panel-default">
+ <!-- Default panel contents -->
- <div class="form-group" v-if="isTokenAuth">
- <p>{{$t('login.description')}}</p>
- </div>
+ <div class="panel-heading">
+ {{ $t('login.login') }}
+ </div>
- <div class='form-group'>
- <div class='login-bottom'>
- <div>
- <router-link :to="{name: 'registration'}"
- v-if='registrationOpen'
- class='register'>
- {{$t('login.register')}}
- </router-link>
+ <div class="panel-body">
+ <form
+ class="login-form"
+ @submit.prevent="submit"
+ >
+ <template v-if="isPasswordAuth">
+ <div class="form-group">
+ <label for="username">{{ $t('login.username') }}</label>
+ <input
+ id="username"
+ v-model="user.username"
+ :disabled="loggingIn"
+ class="form-control"
+ :placeholder="$t('login.placeholder')"
+ >
</div>
- <button :disabled="loggingIn" type='submit' class='btn btn-default'>
- {{$t('login.login')}}
- </button>
+ <div class="form-group">
+ <label for="password">{{ $t('login.password') }}</label>
+ <input
+ id="password"
+ ref="passwordInput"
+ v-model="user.password"
+ :disabled="loggingIn"
+ class="form-control"
+ type="password"
+ >
+ </div>
+ </template>
+
+ <div
+ v-if="isTokenAuth"
+ class="form-group"
+ >
+ <p>{{ $t('login.description') }}</p>
</div>
- </div>
- </form>
- </div>
- <div v-if="error" class='form-group'>
- <div class='alert error'>
- {{error}}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <div class="form-group">
+ <div class="login-bottom">
+ <div>
+ <router-link
+ v-if="registrationOpen"
+ :to="{name: 'registration'}"
+ class="register"
+ >
+ {{ $t('login.register') }}
+ </router-link>
+ </div>
+ <button
+ :disabled="loggingIn"
+ type="submit"
+ class="btn btn-default"
+ >
+ {{ $t('login.login') }}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <div
+ v-if="error"
+ class="form-group"
+ >
+ <div class="alert error">
+ {{ error }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearError"
+ />
+ </div>
</div>
</div>
-</div>
</template>
<script src="./login_form.js" ></script>
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index a4c12d74..0543e677 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -1,25 +1,33 @@
<template>
- <div class="modal-view media-modal-view" v-if="showing" @click.prevent="hide">
- <img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img>
- <VideoAttachment
+ <div
+ v-if="showing"
+ class="modal-view media-modal-view"
+ @click.prevent="hide"
+ >
+ <img
+ v-if="type === 'image'"
class="modal-image"
+ :src="currentMedia.url"
+ >
+ <VideoAttachment
v-if="type === 'video'"
+ class="modal-image"
:attachment="currentMedia"
:controls="true"
- @click.stop.native="">
- </VideoAttachment>
+ @click.stop.native=""
+ />
<button
+ v-if="canNavigate"
:title="$t('media_modal.previous')"
class="modal-view-button-arrow modal-view-button-arrow--prev"
- v-if="canNavigate"
@click.stop.prevent="goPrev"
>
<i class="icon-left-open arrow-icon" />
</button>
<button
+ v-if="canNavigate"
:title="$t('media_modal.next')"
class="modal-view-button-arrow modal-view-button-arrow--next"
- v-if="canNavigate"
@click.stop.prevent="goNext"
>
<i class="icon-right-open arrow-icon" />
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index e4b3d460..f457d022 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -16,7 +16,7 @@ const mediaUpload = {
if (file.size > store.state.instance.uploadlimit) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
- self.$emit('upload-failed', 'file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
+ self.$emit('upload-failed', 'file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
return
}
const formData = new FormData()
@@ -36,7 +36,7 @@ const mediaUpload = {
},
fileDrop (e) {
if (e.dataTransfer.files.length > 0) {
- e.preventDefault() // allow dropping text like before
+ e.preventDefault() // allow dropping text like before
this.uploadFile(e.dataTransfer.files[0])
}
},
@@ -54,7 +54,7 @@ const mediaUpload = {
this.uploadReady = true
})
},
- change ({target}) {
+ change ({ target }) {
for (var i = 0; i < target.files.length; i++) {
let file = target.files[i]
this.uploadFile(file)
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index fcdc3471..ac32ae83 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -1,9 +1,29 @@
<template>
- <div class="media-upload" @drop.prevent @dragover.prevent="fileDrag" @drop="fileDrop">
- <label class="btn btn-default" :title="$t('tool_tip.media_upload')">
- <i class="icon-spin4 animate-spin" v-if="uploading"></i>
- <i class="icon-upload" v-if="!uploading"></i>
- <input type="file" v-if="uploadReady" @change="change" style="position: fixed; top: -100em" multiple="true"></input>
+ <div
+ class="media-upload"
+ @drop.prevent
+ @dragover.prevent="fileDrag"
+ @drop="fileDrop"
+ >
+ <label
+ class="btn btn-default"
+ :title="$t('tool_tip.media_upload')"
+ >
+ <i
+ v-if="uploading"
+ class="icon-spin4 animate-spin"
+ />
+ <i
+ v-if="!uploading"
+ class="icon-upload"
+ />
+ <input
+ v-if="uploadReady"
+ type="file"
+ style="position: fixed; top: -100em"
+ multiple="true"
+ @change="change"
+ >
</label>
</div>
</template>
@@ -13,7 +33,7 @@
<style>
.media-upload {
font-size: 26px;
- flex: 1;
+ min-width: 50px;
}
.icon-upload {
diff --git a/src/components/mentions/mentions.vue b/src/components/mentions/mentions.vue
index 6b4e96e0..70f60baf 100644
--- a/src/components/mentions/mentions.vue
+++ b/src/components/mentions/mentions.vue
@@ -1,5 +1,9 @@
<template>
- <Timeline :title="$t('nav.interactions')" v-bind:timeline="timeline" v-bind:timeline-name="'mentions'"/>
+ <Timeline
+ :title="$t('nav.interactions')"
+ :timeline="timeline"
+ :timeline-name="'mentions'"
+ />
</template>
<script src="./mentions.js"></script>
diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js
index fbe9b437..7a3cc22d 100644
--- a/src/components/mfa_form/recovery_form.js
+++ b/src/components/mfa_form/recovery_form.js
@@ -33,7 +33,7 @@ export default {
}
this.login(result).then(() => {
- this.$router.push({name: 'friends'})
+ this.$router.push({ name: 'friends' })
})
})
}
diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue
index e0e2d65b..57294630 100644
--- a/src/components/mfa_form/recovery_form.vue
+++ b/src/components/mfa_form/recovery_form.vue
@@ -1,42 +1,65 @@
<template>
-<div class="login panel panel-default">
- <!-- Default panel contents -->
+ <div class="login panel panel-default">
+ <!-- Default panel contents -->
- <div class="panel-heading">{{$t('login.heading.recovery')}}</div>
+ <div class="panel-heading">
+ {{ $t('login.heading.recovery') }}
+ </div>
- <div class="panel-body">
- <form class='login-form' @submit.prevent='submit'>
- <div class='form-group'>
- <label for='code'>{{$t('login.recovery_code')}}</label>
- <input v-model='code' class='form-control' id='code'>
- </div>
+ <div class="panel-body">
+ <form
+ class="login-form"
+ @submit.prevent="submit"
+ >
+ <div class="form-group">
+ <label for="code">{{ $t('login.recovery_code') }}</label>
+ <input
+ id="code"
+ v-model="code"
+ class="form-control"
+ >
+ </div>
- <div class='form-group'>
- <div class='login-bottom'>
- <div>
- <a href="#" @click.prevent="requireTOTP">
- {{$t('login.enter_two_factor_code')}}
- </a>
- <br />
- <a href="#" @click.prevent="abortMFA">
- {{$t('general.cancel')}}
- </a>
+ <div class="form-group">
+ <div class="login-bottom">
+ <div>
+ <a
+ href="#"
+ @click.prevent="requireTOTP"
+ >
+ {{ $t('login.enter_two_factor_code') }}
+ </a>
+ <br>
+ <a
+ href="#"
+ @click.prevent="abortMFA"
+ >
+ {{ $t('general.cancel') }}
+ </a>
+ </div>
+ <button
+ type="submit"
+ class="btn btn-default"
+ >
+ {{ $t('general.verify') }}
+ </button>
</div>
- <button type='submit' class='btn btn-default'>
- {{$t('general.verify')}}
- </button>
</div>
- </div>
-
- </form>
- </div>
+ </form>
+ </div>
- <div v-if="error" class='form-group'>
- <div class='alert error'>
- {{error}}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <div
+ v-if="error"
+ class="form-group"
+ >
+ <div class="alert error">
+ {{ error }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearError"
+ />
+ </div>
</div>
</div>
-</div>
</template>
<script src="./recovery_form.js" ></script>
diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js
index 6c94fe52..778bf8dc 100644
--- a/src/components/mfa_form/totp_form.js
+++ b/src/components/mfa_form/totp_form.js
@@ -32,7 +32,7 @@ export default {
}
this.login(result).then(() => {
- this.$router.push({name: 'friends'})
+ this.$router.push({ name: 'friends' })
})
})
}
diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue
index c547785e..a344b395 100644
--- a/src/components/mfa_form/totp_form.vue
+++ b/src/components/mfa_form/totp_form.vue
@@ -1,45 +1,67 @@
<template>
-<div class="login panel panel-default">
- <!-- Default panel contents -->
+ <div class="login panel panel-default">
+ <!-- Default panel contents -->
- <div class="panel-heading">
- {{$t('login.heading.totp')}}
- </div>
+ <div class="panel-heading">
+ {{ $t('login.heading.totp') }}
+ </div>
- <div class="panel-body">
- <form class='login-form' @submit.prevent='submit'>
- <div class='form-group'>
- <label for='code'>
- {{$t('login.authentication_code')}}
- </label>
- <input v-model='code' class='form-control' id='code'>
- </div>
+ <div class="panel-body">
+ <form
+ class="login-form"
+ @submit.prevent="submit"
+ >
+ <div class="form-group">
+ <label for="code">
+ {{ $t('login.authentication_code') }}
+ </label>
+ <input
+ id="code"
+ v-model="code"
+ class="form-control"
+ >
+ </div>
- <div class='form-group'>
- <div class='login-bottom'>
- <div>
- <a href="#" @click.prevent="requireRecovery">
- {{$t('login.enter_recovery_code')}}
- </a>
- <br />
- <a href="#" @click.prevent="abortMFA">
- {{$t('general.cancel')}}
- </a>
+ <div class="form-group">
+ <div class="login-bottom">
+ <div>
+ <a
+ href="#"
+ @click.prevent="requireRecovery"
+ >
+ {{ $t('login.enter_recovery_code') }}
+ </a>
+ <br>
+ <a
+ href="#"
+ @click.prevent="abortMFA"
+ >
+ {{ $t('general.cancel') }}
+ </a>
+ </div>
+ <button
+ type="submit"
+ class="btn btn-default"
+ >
+ {{ $t('general.verify') }}
+ </button>
</div>
- <button type='submit' class='btn btn-default'>
- {{$t('general.verify')}}
- </button>
</div>
- </div>
- </form>
- </div>
+ </form>
+ </div>
- <div v-if="error" class='form-group'>
- <div class='alert error'>
- {{error}}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <div
+ v-if="error"
+ class="form-group"
+ >
+ <div class="alert error">
+ {{ error }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearError"
+ />
+ </div>
</div>
</div>
-</div>
</template>
<script src="./totp_form.js"></script>
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index 9b341a3b..c2bb76ee 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -1,14 +1,12 @@
import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue'
-import MobilePostStatusModal from '../mobile_post_status_modal/mobile_post_status_modal.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service'
const MobileNav = {
components: {
SideDrawer,
- Notifications,
- MobilePostStatusModal
+ Notifications
},
data: () => ({
notificationsCloseGesture: undefined,
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index dcac440a..d1c24e56 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -1,39 +1,75 @@
<template>
<div>
- <nav class='nav-bar container' id="nav">
- <div class='mobile-inner-nav' @click="scrollToTop()">
- <div class='item'>
- <a href="#" class="mobile-nav-button" @click.stop.prevent="toggleMobileSidebar()">
- <i class="button-icon icon-menu"></i>
+ <nav
+ id="nav"
+ class="nav-bar container"
+ >
+ <div
+ class="mobile-inner-nav"
+ @click="scrollToTop()"
+ >
+ <div class="item">
+ <a
+ href="#"
+ class="mobile-nav-button"
+ @click.stop.prevent="toggleMobileSidebar()"
+ >
+ <i class="button-icon icon-menu" />
</a>
- <router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
+ <router-link
+ class="site-name"
+ :to="{ name: 'root' }"
+ active-class="home"
+ >
+ {{ sitename }}
+ </router-link>
</div>
- <div class='item right'>
- <a class="mobile-nav-button" v-if="currentUser" href="#" @click.stop.prevent="openMobileNotifications()">
- <i class="button-icon icon-bell-alt"></i>
- <div class="alert-dot" v-if="unseenNotificationsCount"></div>
+ <div class="item right">
+ <a
+ v-if="currentUser"
+ class="mobile-nav-button"
+ href="#"
+ @click.stop.prevent="openMobileNotifications()"
+ >
+ <i class="button-icon icon-bell-alt" />
+ <div
+ v-if="unseenNotificationsCount"
+ class="alert-dot"
+ />
</a>
</div>
</div>
</nav>
- <div v-if="currentUser"
+ <div
+ v-if="currentUser"
class="mobile-notifications-drawer"
:class="{ 'closed': !notificationsOpen }"
@touchstart.stop="notificationsTouchStart"
@touchmove.stop="notificationsTouchMove"
>
<div class="mobile-notifications-header">
- <span class="title">{{$t('notifications.notifications')}}</span>
- <a class="mobile-nav-button" @click.stop.prevent="closeMobileNotifications()">
- <i class="button-icon icon-cancel"/>
+ <span class="title">{{ $t('notifications.notifications') }}</span>
+ <a
+ class="mobile-nav-button"
+ @click.stop.prevent="closeMobileNotifications()"
+ >
+ <i class="button-icon icon-cancel" />
</a>
</div>
- <div class="mobile-notifications" @scroll="onScroll">
- <Notifications ref="notifications" :noHeading="true"/>
+ <div
+ class="mobile-notifications"
+ @scroll="onScroll"
+ >
+ <Notifications
+ ref="notifications"
+ :no-heading="true"
+ />
</div>
</div>
- <SideDrawer ref="sideDrawer" :logout="logout"/>
- <MobilePostStatusModal />
+ <SideDrawer
+ ref="sideDrawer"
+ :logout="logout"
+ />
</div>
</template>
diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.js b/src/components/mobile_post_status_modal/mobile_post_status_modal.js
index 91b730e7..3cec23c6 100644
--- a/src/components/mobile_post_status_modal/mobile_post_status_modal.js
+++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.js
@@ -96,12 +96,12 @@ const MobilePostStatusModal = {
this.hidden = false
}
this.oldScrollPos = window.scrollY
- }, 100, {leading: true, trailing: false}),
+ }, 100, { leading: true, trailing: false }),
handleScrollEnd: debounce(function () {
this.hidden = false
this.oldScrollPos = window.scrollY
- }, 100, {leading: false, trailing: true})
+ }, 100, { leading: false, trailing: true })
}
}
diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
index c762705b..b6d7d3ba 100644
--- a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
+++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue
@@ -1,23 +1,31 @@
<template>
-<div v-if="currentUser">
- <div
- class="post-form-modal-view modal-view"
- v-show="postFormOpen"
- @click="closePostForm"
- >
- <div class="post-form-modal-panel panel" @click.stop="">
- <div class="panel-heading">{{$t('post_status.new_status')}}</div>
- <PostStatusForm class="panel-body" @posted="closePostForm" />
+ <div v-if="currentUser">
+ <div
+ v-show="postFormOpen"
+ class="post-form-modal-view modal-view"
+ @click="closePostForm"
+ >
+ <div
+ class="post-form-modal-panel panel"
+ @click.stop=""
+ >
+ <div class="panel-heading">
+ {{ $t('post_status.new_status') }}
+ </div>
+ <PostStatusForm
+ class="panel-body"
+ @posted="closePostForm"
+ />
+ </div>
</div>
+ <button
+ class="new-status-button"
+ :class="{ 'hidden': isHidden }"
+ @click="openPostForm"
+ >
+ <i class="icon-edit" />
+ </button>
</div>
- <button
- class="new-status-button"
- :class="{ 'hidden': isHidden }"
- @click="openPostForm"
- >
- <i class="icon-edit" />
- </button>
-</div>
</template>
<script src="./mobile_post_status_modal.js"></script>
@@ -26,14 +34,19 @@
@import '../../_variables.scss';
.post-form-modal-view {
- max-height: 100%;
- display: block;
+ align-items: flex-start;
}
.post-form-modal-panel {
flex-shrink: 0;
- margin: 25% 0 4em 0;
+ margin-top: 25%;
+ margin-bottom: 2em;
width: 100%;
+ max-width: 700px;
+
+ @media (orientation: landscape) {
+ margin-top: 8%;
+ }
}
.new-status-button {
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
index 3eedeaa1..8aadc8c5 100644
--- a/src/components/moderation_tools/moderation_tools.js
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -1,5 +1,4 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
-import Popper from 'vue-popperjs/src/component/popper.js.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
@@ -29,8 +28,7 @@ const ModerationTools = {
}
},
components: {
- DialogModal,
- Popper
+ DialogModal
},
computed: {
tagsSet () {
@@ -41,9 +39,6 @@ const ModerationTools = {
}
},
methods: {
- toggleMenu () {
- this.showDropDown = !this.showDropDown
- },
hasTag (tagName) {
return this.tagsSet.has(tagName)
},
@@ -52,12 +47,12 @@ const ModerationTools = {
if (this.tagsSet.has(tag)) {
store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
if (!response.ok) { return }
- store.commit('untagUser', {user: this.user, tag})
+ store.commit('untagUser', { user: this.user, tag })
})
} else {
store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
if (!response.ok) { return }
- store.commit('tagUser', {user: this.user, tag})
+ store.commit('tagUser', { user: this.user, tag })
})
}
},
@@ -66,12 +61,12 @@ const ModerationTools = {
if (this.user.rights[right]) {
store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
if (!response.ok) { return }
- store.commit('updateRight', {user: this.user, right: right, value: false})
+ store.commit('updateRight', { user: this.user, right: right, value: false })
})
} else {
store.state.api.backendInteractor.addRight(this.user, right).then(response => {
if (!response.ok) { return }
- store.commit('updateRight', {user: this.user, right: right, value: true})
+ store.commit('updateRight', { user: this.user, right: right, value: true })
})
}
},
@@ -80,7 +75,7 @@ const ModerationTools = {
const status = !!this.user.deactivated
store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
if (!response.ok) { return }
- store.commit('updateActivationStatus', {user: this.user, status: status})
+ store.commit('updateActivationStatus', { user: this.user, status: status })
})
},
deleteUserDialog (show) {
@@ -89,7 +84,7 @@ const ModerationTools = {
deleteUser () {
const store = this.$store
const user = this.user
- const {id, name} = user
+ const { id, name } = user
store.state.api.backendInteractor.deleteUser(user)
.then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index 6a5e8cc0..d97ca3aa 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -1,85 +1,161 @@
<template>
-<div class='block' style='position: relative'>
- <Popper
- trigger="click"
- @hide='showDropDown = false'
- append-to-body
- :options="{
- placement: 'bottom-end',
- modifiers: {
- arrow: { enabled: true },
- offset: { offset: '0, 5px' },
- }
- }">
- <div class="popper-wrapper">
- <div class="dropdown-menu">
- <span v-if='user.is_local'>
- <button class="dropdown-item" @click='toggleRight("admin")'>
- {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
+ <div>
+ <v-popover
+ trigger="click"
+ class="moderation-tools-popover"
+ :container="false"
+ placement="bottom-end"
+ :offset="5"
+ @show="showDropDown = true"
+ @hide="showDropDown = false"
+ >
+ <div slot="popover">
+ <div class="dropdown-menu">
+ <span v-if="user.is_local">
+ <button
+ class="dropdown-item"
+ @click="toggleRight(&quot;admin&quot;)"
+ >
+ {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
+ </button>
+ <button
+ class="dropdown-item"
+ @click="toggleRight(&quot;moderator&quot;)"
+ >
+ {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
+ </button>
+ <div
+ role="separator"
+ class="dropdown-divider"
+ />
+ </span>
+ <button
+ class="dropdown-item"
+ @click="toggleActivationStatus()"
+ >
+ {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
- <button class="dropdown-item" @click='toggleRight("moderator")'>
- {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
+ <button
+ class="dropdown-item"
+ @click="deleteUserDialog(true)"
+ >
+ {{ $t('user_card.admin_menu.delete_account') }}
</button>
- <div role="separator" class="dropdown-divider"></div>
- </span>
- <button class="dropdown-item" @click='toggleActivationStatus()'>
- {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
- </button>
- <button class="dropdown-item" @click='deleteUserDialog(true)'>
- {{ $t('user_card.admin_menu.delete_account') }}
- </button>
- <div role="separator" class="dropdown-divider" v-if='hasTagPolicy'></div>
- <span v-if='hasTagPolicy'>
- <button class="dropdown-item" @click='toggleTag(tags.FORCE_NSFW)'>
- {{ $t('user_card.admin_menu.force_nsfw') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"></span>
- </button>
- <button class="dropdown-item" @click='toggleTag(tags.STRIP_MEDIA)'>
- {{ $t('user_card.admin_menu.strip_media') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"></span>
- </button>
- <button class="dropdown-item" @click='toggleTag(tags.FORCE_UNLISTED)'>
- {{ $t('user_card.admin_menu.force_unlisted') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"></span>
- </button>
- <button class="dropdown-item" @click='toggleTag(tags.SANDBOX)'>
- {{ $t('user_card.admin_menu.sandbox') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"></span>
- </button>
- <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)'>
- {{ $t('user_card.admin_menu.disable_remote_subscription') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"></span>
- </button>
- <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)'>
- {{ $t('user_card.admin_menu.disable_any_subscription') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"></span>
- </button>
- <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.QUARANTINE)'>
- {{ $t('user_card.admin_menu.quarantine') }}
- <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"></span>
- </button>
- </span>
+ <div
+ v-if="hasTagPolicy"
+ role="separator"
+ class="dropdown-divider"
+ />
+ <span v-if="hasTagPolicy">
+ <button
+ class="dropdown-item"
+ @click="toggleTag(tags.FORCE_NSFW)"
+ >
+ {{ $t('user_card.admin_menu.force_nsfw') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
+ />
+ </button>
+ <button
+ class="dropdown-item"
+ @click="toggleTag(tags.STRIP_MEDIA)"
+ >
+ {{ $t('user_card.admin_menu.strip_media') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
+ />
+ </button>
+ <button
+ class="dropdown-item"
+ @click="toggleTag(tags.FORCE_UNLISTED)"
+ >
+ {{ $t('user_card.admin_menu.force_unlisted') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
+ />
+ </button>
+ <button
+ class="dropdown-item"
+ @click="toggleTag(tags.SANDBOX)"
+ >
+ {{ $t('user_card.admin_menu.sandbox') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
+ />
+ </button>
+ <button
+ v-if="user.is_local"
+ class="dropdown-item"
+ @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
+ >
+ {{ $t('user_card.admin_menu.disable_remote_subscription') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
+ />
+ </button>
+ <button
+ v-if="user.is_local"
+ class="dropdown-item"
+ @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
+ >
+ {{ $t('user_card.admin_menu.disable_any_subscription') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
+ />
+ </button>
+ <button
+ v-if="user.is_local"
+ class="dropdown-item"
+ @click="toggleTag(tags.QUARANTINE)"
+ >
+ {{ $t('user_card.admin_menu.quarantine') }}
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
+ />
+ </button>
+ </span>
+ </div>
</div>
- </div>
- <button slot="reference" v-bind:class="{ pressed: showDropDown }" @click='toggleMenu'>
- {{ $t('user_card.admin_menu.moderation') }}
- </button>
- </Popper>
- <portal to="modal">
- <DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
- <template slot="header">{{ $t('user_card.admin_menu.delete_user') }}</template>
- <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
- <template slot="footer">
- <button class="btn btn-default" @click='deleteUserDialog(false)'>
- {{ $t('general.cancel') }}
- </button>
- <button class="btn btn-default danger" @click='deleteUser()'>
+ <button
+ class="btn btn-default btn-block"
+ :class="{ pressed: showDropDown }"
+ >
+ {{ $t('user_card.admin_menu.moderation') }}
+ </button>
+ </v-popover>
+ <portal to="modal">
+ <DialogModal
+ v-if="showDeleteUserDialog"
+ :on-cancel="deleteUserDialog.bind(this, false)"
+ >
+ <template slot="header">
{{ $t('user_card.admin_menu.delete_user') }}
- </button>
- </template>
- </DialogModal>
- </portal>
-</div>
+ </template>
+ <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
+ <template slot="footer">
+ <button
+ class="btn btn-default"
+ @click="deleteUserDialog(false)"
+ >
+ {{ $t('general.cancel') }}
+ </button>
+ <button
+ class="btn btn-default danger"
+ @click="deleteUser()"
+ >
+ {{ $t('user_card.admin_menu.delete_user') }}
+ </button>
+ </template>
+ </DialogModal>
+ </portal>
+ </div>
</template>
<script src="./moderation_tools.js"></script>
@@ -107,4 +183,11 @@
}
}
+.moderation-tools-popover {
+ height: 100%;
+ .trigger {
+ display: flex !important;
+ height: 100%;
+ }
+}
</style>
diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
index a4edff03..9611fb82 100644
--- a/src/components/mute_card/mute_card.vue
+++ b/src/components/mute_card/mute_card.vue
@@ -1,7 +1,12 @@
<template>
<basic-user-card :user="user">
<div class="mute-card-content-container">
- <button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
+ <button
+ v-if="muted"
+ class="btn btn-default"
+ :disabled="progress"
+ @click="unmuteUser"
+ >
<template v-if="progress">
{{ $t('user_card.unmute_progress') }}
</template>
@@ -9,7 +14,12 @@
{{ $t('user_card.unmute') }}
</template>
</button>
- <button class="btn btn-default" @click="muteUser" :disabled="progress" v-else>
+ <button
+ v-else
+ class="btn btn-default"
+ :disabled="progress"
+ @click="muteUser"
+ >
<template v-if="progress">
{{ $t('user_card.mute_progress') }}
</template>
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index e6e0f074..614fadf4 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -2,26 +2,29 @@
<div class="nav-panel">
<div class="panel panel-default">
<ul>
- <li v-if='currentUser'>
+ <li v-if="currentUser">
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
</router-link>
</li>
- <li v-if='currentUser'>
+ <li v-if="currentUser">
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
{{ $t("nav.interactions") }}
</router-link>
</li>
- <li v-if='currentUser'>
+ <li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
</router-link>
</li>
- <li v-if='currentUser && currentUser.locked'>
+ <li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }">
- {{ $t("nav.friend_requests")}}
- <span v-if='followRequestCount > 0' class="badge follow-request-count">
- {{followRequestCount}}
+ {{ $t("nav.friend_requests") }}
+ <span
+ v-if="followRequestCount > 0"
+ class="badge follow-request-count"
+ >
+ {{ followRequestCount }}
</span>
</router-link>
</li>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index e59e7497..896c6d52 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
+import Timeago from '../timeago/timeago.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -13,7 +14,10 @@ const Notification = {
},
props: [ 'notification' ],
components: {
- Status, UserAvatar, UserCard
+ Status,
+ UserAvatar,
+ UserCard,
+ Timeago
},
methods: {
toggleUserExpanded () {
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 3427b9c5..bafcd026 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -3,49 +3,104 @@
v-if="notification.type === 'mention'"
:compact="true"
:statusoid="notification.status"
+ />
+ <div
+ v-else
+ class="non-mention"
+ :class="[userClass, { highlighted: userStyle }]"
+ :style="[ userStyle ]"
>
- </status>
- <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
- <a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
- <UserAvatar :compact="true" :betterShadow="betterShadow" :user="notification.from_profile"/>
+ <a
+ class="avatar-container"
+ :href="notification.from_profile.statusnet_profile_url"
+ @click.stop.prevent.capture="toggleUserExpanded"
+ >
+ <UserAvatar
+ :compact="true"
+ :better-shadow="betterShadow"
+ :user="notification.from_profile"
+ />
</a>
- <div class='notification-right'>
- <UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />
+ <div class="notification-right">
+ <UserCard
+ v-if="userExpanded"
+ :user="getUser(notification)"
+ :rounded="true"
+ :bordered="true"
+ />
<span class="notification-details">
<div class="name-and-action">
- <span class="username" v-if="!!notification.from_profile.name_html" :title="'@'+notification.from_profile.screen_name" v-html="notification.from_profile.name_html"></span>
- <span class="username" v-else :title="'@'+notification.from_profile.screen_name">{{ notification.from_profile.name }}</span>
+ <!-- eslint-disable vue/no-v-html -->
+ <span
+ v-if="!!notification.from_profile.name_html"
+ class="username"
+ :title="'@'+notification.from_profile.screen_name"
+ v-html="notification.from_profile.name_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <span
+ v-else
+ class="username"
+ :title="'@'+notification.from_profile.screen_name"
+ >{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'">
- <i class="fa icon-star lit"></i>
- <small>{{$t('notifications.favorited_you')}}</small>
+ <i class="fa icon-star lit" />
+ <small>{{ $t('notifications.favorited_you') }}</small>
</span>
<span v-if="notification.type === 'repeat'">
- <i class="fa icon-retweet lit" :title="$t('tool_tip.repeat')"></i>
- <small>{{$t('notifications.repeated_you')}}</small>
+ <i
+ class="fa icon-retweet lit"
+ :title="$t('tool_tip.repeat')"
+ />
+ <small>{{ $t('notifications.repeated_you') }}</small>
</span>
<span v-if="notification.type === 'follow'">
- <i class="fa icon-user-plus lit"></i>
- <small>{{$t('notifications.followed_you')}}</small>
+ <i class="fa icon-user-plus lit" />
+ <small>{{ $t('notifications.followed_you') }}</small>
</span>
</div>
- <div class="timeago" v-if="notification.type === 'follow'">
+ <div
+ v-if="notification.type === 'follow'"
+ class="timeago"
+ >
<span class="faint">
- <timeago :since="notification.created_at" :auto-update="240"></timeago>
+ <Timeago
+ :time="notification.created_at"
+ :auto-update="240"
+ />
</span>
</div>
- <div class="timeago" v-else>
- <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
- <timeago :since="notification.created_at" :auto-update="240"></timeago>
+ <div
+ v-else
+ class="timeago"
+ >
+ <router-link
+ v-if="notification.status"
+ :to="{ name: 'conversation', params: { id: notification.status.id } }"
+ class="faint-link"
+ >
+ <Timeago
+ :time="notification.created_at"
+ :auto-update="240"
+ />
</router-link>
</div>
</span>
- <div class="follow-text" v-if="notification.type === 'follow'">
+ <div
+ v-if="notification.type === 'follow'"
+ class="follow-text"
+ >
<router-link :to="userProfileLink(notification.from_profile)">
- @{{notification.from_profile.screen_name}}
+ @{{ notification.from_profile.screen_name }}
</router-link>
</div>
<template v-else>
- <status class="faint" :compact="true" :statusoid="notification.action" :noHeading="true"></status>
+ <status
+ class="faint"
+ :compact="true"
+ :statusoid="notification.action"
+ :no-heading="true"
+ />
</template>
</div>
</div>
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index c71499b2..c42c35e6 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -1,33 +1,67 @@
<template>
- <div :class="{ minimal: minimalMode }" class="notifications">
+ <div
+ :class="{ minimal: minimalMode }"
+ class="notifications"
+ >
<div :class="mainClass">
- <div v-if="!noHeading" class="panel-heading">
+ <div
+ v-if="!noHeading"
+ class="panel-heading"
+ >
<div class="title">
- {{$t('notifications.notifications')}}
- <span class="badge badge-notification unseen-count" v-if="unseenCount">{{unseenCount}}</span>
+ {{ $t('notifications.notifications') }}
+ <span
+ v-if="unseenCount"
+ class="badge badge-notification unseen-count"
+ >{{ unseenCount }}</span>
</div>
- <div @click.prevent class="loadmore-error alert error" v-if="error">
- {{$t('timeline.error_fetching')}}
+ <div
+ v-if="error"
+ class="loadmore-error alert error"
+ @click.prevent
+ >
+ {{ $t('timeline.error_fetching') }}
</div>
- <button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
+ <button
+ v-if="unseenCount"
+ class="read-button"
+ @click.prevent="markAsSeen"
+ >
+ {{ $t('notifications.read') }}
+ </button>
</div>
<div class="panel-body">
- <div v-for="notification in visibleNotifications" :key="notification.id" class="notification" :class='{"unseen": !minimalMode && !notification.seen}'>
- <div class="notification-overlay"></div>
- <notification :notification="notification"></notification>
+ <div
+ v-for="notification in visibleNotifications"
+ :key="notification.id"
+ class="notification"
+ :class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
+ >
+ <div class="notification-overlay" />
+ <notification :notification="notification" />
</div>
</div>
<div class="panel-footer">
- <div v-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
- {{$t('notifications.no_more_notifications')}}
+ <div
+ v-if="bottomedOut"
+ class="new-status-notification text-center panel-footer faint"
+ >
+ {{ $t('notifications.no_more_notifications') }}
</div>
- <a v-else-if="!loading" href="#" v-on:click.prevent="fetchOlderNotifications()">
+ <a
+ v-else-if="!loading"
+ href="#"
+ @click.prevent="fetchOlderNotifications()"
+ >
<div class="new-status-notification text-center panel-footer">
- {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older')}}
+ {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
</div>
</a>
- <div v-else class="new-status-notification text-center panel-footer">
- <i class="icon-spin3 animate-spin"/>
+ <div
+ v-else
+ class="new-status-notification text-center panel-footer"
+ >
+ <i class="icon-spin3 animate-spin" />
</div>
</div>
</div>
diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js
index 2c6ca235..a3c7b7f9 100644
--- a/src/components/oauth_callback/oauth_callback.js
+++ b/src/components/oauth_callback/oauth_callback.js
@@ -4,10 +4,11 @@ const oac = {
props: ['code'],
mounted () {
if (this.code) {
- const { clientId } = this.$store.state.oauth
+ const { clientId, clientSecret } = this.$store.state.oauth
oauth.getToken({
clientId,
+ clientSecret,
instance: this.$store.state.instance.server,
code: this.code
}).then((result) => {
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index 3926915b..c677f18c 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -1,27 +1,39 @@
<template>
-<div class="opacity-control style-control" :class="{ disabled: !present || disabled }">
- <label :for="name" class="label">
- {{$t('settings.style.common.opacity')}}
- </label>
- <input
- v-if="typeof fallback !== 'undefined'"
- class="opt exclude-disabled"
- :id="name + '-o'"
- type="checkbox"
- :checked="present"
- @input="$emit('input', !present ? fallback : undefined)">
- <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
- <input
- :id="name"
- class="input-number"
- type="number"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
- max="1"
- min="0"
- step=".05">
-</div>
+ <div
+ class="opacity-control style-control"
+ :class="{ disabled: !present || disabled }"
+ >
+ <label
+ :for="name"
+ class="label"
+ >
+ {{ $t('settings.style.common.opacity') }}
+ </label>
+ <input
+ v-if="typeof fallback !== 'undefined'"
+ :id="name + '-o'"
+ class="opt exclude-disabled"
+ type="checkbox"
+ :checked="present"
+ @input="$emit('input', !present ? fallback : undefined)"
+ >
+ <label
+ v-if="typeof fallback !== 'undefined'"
+ class="opt-l"
+ :for="name + '-o'"
+ />
+ <input
+ :id="name"
+ class="input-number"
+ type="number"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ max="1"
+ min="0"
+ step=".05"
+ @input="$emit('input', $event.target.value)"
+ >
+ </div>
</template>
<script>
diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js
new file mode 100644
index 00000000..98db5582
--- /dev/null
+++ b/src/components/poll/poll.js
@@ -0,0 +1,112 @@
+import Timeago from '../timeago/timeago.vue'
+import { forEach, map } from 'lodash'
+
+export default {
+ name: 'Poll',
+ props: ['basePoll'],
+ components: { Timeago },
+ data () {
+ return {
+ loading: false,
+ choices: []
+ }
+ },
+ created () {
+ if (!this.$store.state.polls.pollsObject[this.pollId]) {
+ this.$store.dispatch('mergeOrAddPoll', this.basePoll)
+ }
+ this.$store.dispatch('trackPoll', this.pollId)
+ },
+ destroyed () {
+ this.$store.dispatch('untrackPoll', this.pollId)
+ },
+ computed: {
+ pollId () {
+ return this.basePoll.id
+ },
+ poll () {
+ const storePoll = this.$store.state.polls.pollsObject[this.pollId]
+ return storePoll || {}
+ },
+ options () {
+ return (this.poll && this.poll.options) || []
+ },
+ expiresAt () {
+ return (this.poll && this.poll.expires_at) || 0
+ },
+ expired () {
+ return (this.poll && this.poll.expired) || false
+ },
+ loggedIn () {
+ return this.$store.state.users.currentUser
+ },
+ showResults () {
+ return this.poll.voted || this.expired || !this.loggedIn
+ },
+ totalVotesCount () {
+ return this.poll.votes_count
+ },
+ containerClass () {
+ return {
+ loading: this.loading
+ }
+ },
+ choiceIndices () {
+ // Convert array of booleans into an array of indices of the
+ // items that were 'true', so [true, false, false, true] becomes
+ // [0, 3].
+ return this.choices
+ .map((entry, index) => entry && index)
+ .filter(value => typeof value === 'number')
+ },
+ isDisabled () {
+ const noChoice = this.choiceIndices.length === 0
+ return this.loading || noChoice
+ }
+ },
+ methods: {
+ percentageForOption (count) {
+ return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
+ },
+ resultTitle (option) {
+ return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
+ },
+ fetchPoll () {
+ this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
+ },
+ activateOption (index) {
+ // forgive me father: doing checking the radio/checkboxes
+ // in code because of customized input elements need either
+ // a) an extra element for the actual graphic, or b) use a
+ // pseudo element for the label. We use b) which mandates
+ // using "for" and "id" matching which isn't nice when the
+ // same poll appears multiple times on the site (notifs and
+ // timeline for example). With code we can make sure it just
+ // works without altering the pseudo element implementation.
+ const allElements = this.$el.querySelectorAll('input')
+ const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
+ if (this.poll.multiple) {
+ // Checkboxes, toggle only the clicked one
+ clickedElement.checked = !clickedElement.checked
+ } else {
+ // Radio button, uncheck everything and check the clicked one
+ forEach(allElements, element => { element.checked = false })
+ clickedElement.checked = true
+ }
+ this.choices = map(allElements, e => e.checked)
+ },
+ optionId (index) {
+ return `poll${this.poll.id}-${index}`
+ },
+ vote () {
+ if (this.choiceIndices.length === 0) return
+ this.loading = true
+ this.$store.dispatch(
+ 'votePoll',
+ { id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }
+ ).then(poll => {
+ this.loading = false
+ })
+ }
+ }
+}
diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue
new file mode 100644
index 00000000..db8e33b3
--- /dev/null
+++ b/src/components/poll/poll.vue
@@ -0,0 +1,134 @@
+<template>
+ <div
+ class="poll"
+ :class="containerClass"
+ >
+ <div
+ v-for="(option, index) in options"
+ :key="index"
+ class="poll-option"
+ >
+ <div
+ v-if="showResults"
+ :title="resultTitle(option)"
+ class="option-result"
+ >
+ <div class="option-result-label">
+ <span class="result-percentage">
+ {{ percentageForOption(option.votes_count) }}%
+ </span>
+ <span>{{ option.title }}</span>
+ </div>
+ <div
+ class="result-fill"
+ :style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
+ />
+ </div>
+ <div
+ v-else
+ @click="activateOption(index)"
+ >
+ <input
+ v-if="poll.multiple"
+ type="checkbox"
+ :disabled="loading"
+ :value="index"
+ >
+ <input
+ v-else
+ type="radio"
+ :disabled="loading"
+ :value="index"
+ >
+ <label class="option-vote">
+ <div>{{ option.title }}</div>
+ </label>
+ </div>
+ </div>
+ <div class="footer faint">
+ <button
+ v-if="!showResults"
+ class="btn btn-default poll-vote-button"
+ type="button"
+ :disabled="isDisabled"
+ @click="vote"
+ >
+ {{ $t('polls.vote') }}
+ </button>
+ <div class="total">
+ {{ totalVotesCount }} {{ $t("polls.votes") }}&nbsp;·&nbsp;
+ </div>
+ <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
+ <Timeago
+ :time="expiresAt"
+ :auto-update="60"
+ :now-threshold="0"
+ />
+ </i18n>
+ </div>
+ </div>
+</template>
+
+<script src="./poll.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.poll {
+ .votes {
+ display: flex;
+ flex-direction: column;
+ margin: 0 0 0.5em;
+ }
+ .poll-option {
+ margin: 0.75em 0.5em;
+ }
+ .option-result {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ position: relative;
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ .option-result-label {
+ display: flex;
+ align-items: center;
+ padding: 0.1em 0.25em;
+ z-index: 1;
+ }
+ .result-percentage {
+ width: 3.5em;
+ flex-shrink: 0;
+ }
+ .result-fill {
+ height: 100%;
+ position: absolute;
+ background-color: $fallback--lightBg;
+ background-color: var(--linkBg, $fallback--lightBg);
+ border-radius: $fallback--panelRadius;
+ border-radius: var(--panelRadius, $fallback--panelRadius);
+ top: 0;
+ left: 0;
+ transition: width 0.5s;
+ }
+ .option-vote {
+ display: flex;
+ align-items: center;
+ }
+ input {
+ width: 3.5em;
+ }
+ .footer {
+ display: flex;
+ align-items: center;
+ }
+ &.loading * {
+ cursor: progress;
+ }
+ .poll-vote-button {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ }
+}
+</style>
diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js
new file mode 100644
index 00000000..c0c1ccf7
--- /dev/null
+++ b/src/components/poll/poll_form.js
@@ -0,0 +1,121 @@
+import * as DateUtils from 'src/services/date_utils/date_utils.js'
+import { uniq } from 'lodash'
+
+export default {
+ name: 'PollForm',
+ props: ['visible'],
+ data: () => ({
+ pollType: 'single',
+ options: ['', ''],
+ expiryAmount: 10,
+ expiryUnit: 'minutes'
+ }),
+ computed: {
+ pollLimits () {
+ return this.$store.state.instance.pollLimits
+ },
+ maxOptions () {
+ return this.pollLimits.max_options
+ },
+ maxLength () {
+ return this.pollLimits.max_option_chars
+ },
+ expiryUnits () {
+ const allUnits = ['minutes', 'hours', 'days']
+ const expiry = this.convertExpiryFromUnit
+ return allUnits.filter(
+ unit => this.pollLimits.max_expiration >= expiry(unit, 1)
+ )
+ },
+ minExpirationInCurrentUnit () {
+ return Math.ceil(
+ this.convertExpiryToUnit(
+ this.expiryUnit,
+ this.pollLimits.min_expiration
+ )
+ )
+ },
+ maxExpirationInCurrentUnit () {
+ return Math.floor(
+ this.convertExpiryToUnit(
+ this.expiryUnit,
+ this.pollLimits.max_expiration
+ )
+ )
+ }
+ },
+ methods: {
+ clear () {
+ this.pollType = 'single'
+ this.options = ['', '']
+ this.expiryAmount = 10
+ this.expiryUnit = 'minutes'
+ },
+ nextOption (index) {
+ const element = this.$el.querySelector(`#poll-${index + 1}`)
+ if (element) {
+ element.focus()
+ } else {
+ // Try adding an option and try focusing on it
+ const addedOption = this.addOption()
+ if (addedOption) {
+ this.$nextTick(function () {
+ this.nextOption(index)
+ })
+ }
+ }
+ },
+ addOption () {
+ if (this.options.length < this.maxOptions) {
+ this.options.push('')
+ return true
+ }
+ return false
+ },
+ deleteOption (index, event) {
+ if (this.options.length > 2) {
+ this.options.splice(index, 1)
+ }
+ },
+ convertExpiryToUnit (unit, amount) {
+ // Note: we want seconds and not milliseconds
+ switch (unit) {
+ case 'minutes': return (1000 * amount) / DateUtils.MINUTE
+ case 'hours': return (1000 * amount) / DateUtils.HOUR
+ case 'days': return (1000 * amount) / DateUtils.DAY
+ }
+ },
+ convertExpiryFromUnit (unit, amount) {
+ // Note: we want seconds and not milliseconds
+ switch (unit) {
+ case 'minutes': return 0.001 * amount * DateUtils.MINUTE
+ case 'hours': return 0.001 * amount * DateUtils.HOUR
+ case 'days': return 0.001 * amount * DateUtils.DAY
+ }
+ },
+ expiryAmountChange () {
+ this.expiryAmount =
+ Math.max(this.minExpirationInCurrentUnit, this.expiryAmount)
+ this.expiryAmount =
+ Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount)
+ this.updatePollToParent()
+ },
+ updatePollToParent () {
+ const expiresIn = this.convertExpiryFromUnit(
+ this.expiryUnit,
+ this.expiryAmount
+ )
+
+ const options = uniq(this.options.filter(option => option !== ''))
+ if (options.length < 2) {
+ this.$emit('update-poll', { error: this.$t('polls.not_enough_options') })
+ return
+ }
+ this.$emit('update-poll', {
+ options,
+ multiple: this.pollType === 'multiple',
+ expiresIn
+ })
+ }
+ }
+}
diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue
new file mode 100644
index 00000000..d53f3837
--- /dev/null
+++ b/src/components/poll/poll_form.vue
@@ -0,0 +1,163 @@
+<template>
+ <div
+ v-if="visible"
+ class="poll-form"
+ >
+ <div
+ v-for="(option, index) in options"
+ :key="index"
+ class="poll-option"
+ >
+ <div class="input-container">
+ <input
+ :id="`poll-${index}`"
+ v-model="options[index]"
+ class="poll-option-input"
+ type="text"
+ :placeholder="$t('polls.option')"
+ :maxlength="maxLength"
+ @change="updatePollToParent"
+ @keydown.enter.stop.prevent="nextOption(index)"
+ >
+ </div>
+ <div
+ v-if="options.length > 2"
+ class="icon-container"
+ >
+ <i
+ class="icon-cancel"
+ @click="deleteOption(index)"
+ />
+ </div>
+ </div>
+ <a
+ v-if="options.length < maxOptions"
+ class="add-option faint"
+ @click="addOption"
+ >
+ <i class="icon-plus" />
+ {{ $t("polls.add_option") }}
+ </a>
+ <div class="poll-type-expiry">
+ <div
+ class="poll-type"
+ :title="$t('polls.type')"
+ >
+ <label
+ for="poll-type-selector"
+ class="select"
+ >
+ <select
+ v-model="pollType"
+ class="select"
+ @change="updatePollToParent"
+ >
+ <option value="single">{{ $t('polls.single_choice') }}</option>
+ <option value="multiple">{{ $t('polls.multiple_choices') }}</option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
+ <div
+ class="poll-expiry"
+ :title="$t('polls.expiry')"
+ >
+ <input
+ v-model="expiryAmount"
+ type="number"
+ class="expiry-amount hide-number-spinner"
+ :min="minExpirationInCurrentUnit"
+ :max="maxExpirationInCurrentUnit"
+ @change="expiryAmountChange"
+ >
+ <label class="expiry-unit select">
+ <select
+ v-model="expiryUnit"
+ @change="expiryAmountChange"
+ >
+ <option
+ v-for="unit in expiryUnits"
+ :key="unit"
+ :value="unit"
+ >
+ {{ $t(`time.${unit}_short`, ['']) }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./poll_form.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.poll-form {
+ display: flex;
+ flex-direction: column;
+ padding: 0 0.5em 0.5em;
+
+ .add-option {
+ align-self: flex-start;
+ padding-top: 0.25em;
+ cursor: pointer;
+ }
+
+ .poll-option {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 0.25em;
+ }
+
+ .input-container {
+ width: 100%;
+ input {
+ // Hack: dodge the floating X icon
+ padding-right: 2.5em;
+ width: 100%;
+ }
+ }
+
+ .icon-container {
+ // Hack: Move the icon over the input box
+ width: 2em;
+ margin-left: -2em;
+ z-index: 1;
+ }
+
+ .poll-type-expiry {
+ margin-top: 0.5em;
+ display: flex;
+ width: 100%;
+ }
+
+ .poll-type {
+ margin-right: 0.75em;
+ flex: 1 1 60%;
+ .select {
+ border: none;
+ box-shadow: none;
+ background-color: transparent;
+ }
+ }
+
+ .poll-expiry {
+ display: flex;
+
+ .expiry-amount {
+ width: 3em;
+ text-align: right;
+ }
+
+ .expiry-unit {
+ border: none;
+ box-shadow: none;
+ background-color: transparent;
+ }
+ }
+}
+</style>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index cfc5c8e7..279b01be 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -1,71 +1,99 @@
@import '../../_variables.scss';
-.popper-wrapper {
+.tooltip.popover {
z-index: 8;
-}
-.popper-wrapper .popper__arrow {
- width: 0;
- height: 0;
- border-style: solid;
- position: absolute;
- margin: 5px;
-}
+ .popover-inner {
+ box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+ box-shadow: var(--panelShadow);
+ border-radius: $fallback--btnRadius;
+ border-radius: var(--btnRadius, $fallback--btnRadius);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+ }
-.popper-wrapper[x-placement^="top"] {
- margin-bottom: 5px;
-}
+ .popover-arrow {
+ width: 0;
+ height: 0;
+ border-style: solid;
+ position: absolute;
+ margin: 5px;
+ border-color: $fallback--bg;
+ border-color: var(--bg, $fallback--bg);
+ z-index: 1;
+ }
-.popper-wrapper[x-placement^="top"] .popper__arrow {
- border-width: 5px 5px 0 5px;
- border-color: $fallback--bg transparent transparent transparent;
- border-color: var(--bg, $fallback--bg) transparent transparent transparent;
- bottom: -5px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
-}
+ &[x-placement^="top"] {
+ margin-bottom: 5px;
+
+ .popover-arrow {
+ border-width: 5px 5px 0 5px;
+ border-left-color: transparent !important;
+ border-right-color: transparent !important;
+ border-bottom-color: transparent !important;
+ bottom: -5px;
+ left: calc(50% - 5px);
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
-.popper-wrapper[x-placement^="bottom"] {
- margin-top: 5px;
-}
+ &[x-placement^="bottom"] {
+ margin-top: 5px;
+
+ .popover-arrow {
+ border-width: 0 5px 5px 5px;
+ border-left-color: transparent !important;
+ border-right-color: transparent !important;
+ border-top-color: transparent !important;
+ top: -5px;
+ left: calc(50% - 5px);
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ }
-.popper-wrapper[x-placement^="bottom"] .popper__arrow {
- border-width: 0 5px 5px 5px;
- border-color: transparent transparent $fallback--bg transparent;
- border-color: transparent transparent var(--bg, $fallback--bg) transparent;
- top: -5px;
- left: calc(50% - 5px);
- margin-top: 0;
- margin-bottom: 0;
-}
+ &[x-placement^="right"] {
+ margin-left: 5px;
+
+ .popover-arrow {
+ border-width: 5px 5px 5px 0;
+ border-left-color: transparent !important;
+ border-top-color: transparent !important;
+ border-bottom-color: transparent !important;
+ left: -5px;
+ top: calc(50% - 5px);
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
-.popper-wrapper[x-placement^="right"] {
- margin-left: 5px;
-}
+ &[x-placement^="left"] {
+ margin-right: 5px;
-.popper-wrapper[x-placement^="right"] .popper__arrow {
- border-width: 5px 5px 5px 0;
- border-color: transparent $fallback--bg transparent transparent;
- border-color: transparent var(--bg, $fallback--bg) transparent transparent;
- left: -5px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
-}
+ .popover-arrow {
+ border-width: 5px 0 5px 5px;
+ border-top-color: transparent !important;
+ border-right-color: transparent !important;
+ border-bottom-color: transparent !important;
+ right: -5px;
+ top: calc(50% - 5px);
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
-.popper-wrapper[x-placement^="left"] {
- margin-right: 5px;
-}
+ &[aria-hidden='true'] {
+ visibility: hidden;
+ opacity: 0;
+ transition: opacity .15s, visibility .15s;
+ }
-.popper-wrapper[x-placement^="left"] .popper__arrow {
- border-width: 5px 0 5px 5px;
- border-color: transparent transparent transparent $fallback--bg;
- border-color: transparent transparent transparent var(--bg, $fallback--bg);
- right: -5px;
- top: calc(50% - 5px);
- margin-left: 0;
- margin-right: 0;
+ &[aria-hidden='false'] {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity .15s;
+ }
}
.dropdown-menu {
@@ -76,13 +104,6 @@
list-style: none;
max-width: 100vw;
z-index: 10;
- box-shadow: 1px 1px 4px rgba(0,0,0,.6);
- box-shadow: var(--panelShadow);
- border: none;
- border-radius: $fallback--btnRadius;
- border-radius: var(--btnRadius, $fallback--btnRadius);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
.dropdown-divider {
height: 0;
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index cbd2024a..40bbf6d4 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -2,17 +2,19 @@ import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji-input/emoji-input.vue'
+import PollForm from '../poll/poll_form.vue'
+import StickerPicker from '../sticker_picker/sticker_picker.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
-import Completion from '../../services/completion/completion.js'
-import { take, filter, reject, map, uniqBy } from 'lodash'
+import { reject, map, uniqBy } from 'lodash'
+import suggestor from '../emoji-input/suggestor.js'
-const buildMentionsString = ({user, attentions}, currentUser) => {
+const buildMentionsString = ({ user, attentions }, currentUser) => {
let allAttentions = [...attentions]
allAttentions.unshift(user)
allAttentions = uniqBy(allAttentions, 'id')
- allAttentions = reject(allAttentions, {id: currentUser.id})
+ allAttentions = reject(allAttentions, { id: currentUser.id })
let mentions = map(allAttentions, (attention) => {
return `@${attention.screen_name}`
@@ -31,8 +33,10 @@ const PostStatusForm = {
],
components: {
MediaUpload,
- ScopeSelector,
- EmojiInput
+ EmojiInput,
+ PollForm,
+ StickerPicker,
+ ScopeSelector
},
mounted () {
this.resize(this.$refs.textarea)
@@ -48,17 +52,17 @@ const PostStatusForm = {
let statusText = preset || ''
const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined'
- ? this.$store.state.instance.scopeCopy
- : this.$store.state.config.scopeCopy
+ ? this.$store.state.instance.scopeCopy
+ : this.$store.state.config.scopeCopy
if (this.replyTo) {
const currentUser = this.$store.state.users.currentUser
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
- const scope = (this.copyMessageScope && scopeCopy || this.copyMessageScope === 'direct')
- ? this.copyMessageScope
- : this.$store.state.users.currentUser.default_scope
+ const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct')
+ ? this.copyMessageScope
+ : this.$store.state.users.currentUser.default_scope
const contentType = typeof this.$store.state.config.postContentType === 'undefined'
? this.$store.state.instance.postContentType
@@ -75,57 +79,16 @@ const PostStatusForm = {
status: statusText,
nsfw: false,
files: [],
+ poll: {},
visibility: scope,
contentType
},
- caret: 0
+ caret: 0,
+ pollFormVisible: false,
+ stickerPickerVisible: false
}
},
computed: {
- candidates () {
- const firstchar = this.textAtCaret.charAt(0)
- if (firstchar === '@') {
- const query = this.textAtCaret.slice(1).toUpperCase()
- const matchedUsers = filter(this.users, (user) => {
- return user.screen_name.toUpperCase().startsWith(query) ||
- user.name && user.name.toUpperCase().startsWith(query)
- })
- if (matchedUsers.length <= 0) {
- return false
- }
- // eslint-disable-next-line camelcase
- return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
- // eslint-disable-next-line camelcase
- screen_name: `@${screen_name}`,
- name: name,
- img: profile_image_url_original,
- highlighted: index === this.highlighted
- }))
- } else if (firstchar === ':') {
- if (this.textAtCaret === ':') { return }
- const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
- if (matchedEmoji.length <= 0) {
- return false
- }
- return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
- screen_name: `:${shortcode}:`,
- name: '',
- utf: utf || '',
- // eslint-disable-next-line camelcase
- img: utf ? '' : this.$store.state.instance.server + image_url,
- highlighted: index === this.highlighted
- }))
- } else {
- return false
- }
- },
- textAtCaret () {
- return (this.wordAtCaret || {}).word || ''
- },
- wordAtCaret () {
- const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {}
- return word
- },
users () {
return this.$store.state.users.users
},
@@ -134,10 +97,28 @@ const PostStatusForm = {
},
showAllScopes () {
const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined'
- ? this.$store.state.instance.minimalScopesMode
- : this.$store.state.config.minimalScopesMode
+ ? this.$store.state.instance.minimalScopesMode
+ : this.$store.state.config.minimalScopesMode
return !minimalScopesMode
},
+ emojiUserSuggestor () {
+ return suggestor({
+ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ],
+ users: this.$store.state.users.users,
+ updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
+ })
+ },
+ emojiSuggestor () {
+ return suggestor({
+ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ]
+ })
+ },
emoji () {
return this.$store.state.instance.emoji || []
},
@@ -174,71 +155,32 @@ const PostStatusForm = {
return true
}
},
- formattingOptionsEnabled () {
- return this.$store.state.instance.formattingOptionsEnabled
- },
postFormats () {
return this.$store.state.instance.postFormats || []
},
safeDMEnabled () {
return this.$store.state.instance.safeDM
},
+ stickersAvailable () {
+ if (this.$store.state.instance.stickers) {
+ return this.$store.state.instance.stickers.length > 0
+ }
+ return 0
+ },
+ pollsAvailable () {
+ return this.$store.state.instance.pollsAvailable &&
+ this.$store.state.instance.pollLimits.max_options >= 2
+ },
hideScopeNotice () {
return this.$store.state.config.hideScopeNotice
+ },
+ pollContentError () {
+ return this.pollFormVisible &&
+ this.newStatus.poll &&
+ this.newStatus.poll.error
}
},
methods: {
- replace (replacement) {
- this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
- const el = this.$el.querySelector('textarea')
- el.focus()
- this.caret = 0
- },
- replaceCandidate (e) {
- const len = this.candidates.length || 0
- if (this.textAtCaret === ':' || e.ctrlKey) { return }
- if (len > 0) {
- e.preventDefault()
- const candidate = this.candidates[this.highlighted]
- const replacement = candidate.utf || (candidate.screen_name + ' ')
- this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
- const el = this.$el.querySelector('textarea')
- el.focus()
- this.caret = 0
- this.highlighted = 0
- }
- },
- cycleBackward (e) {
- const len = this.candidates.length || 0
- if (len > 0) {
- e.preventDefault()
- this.highlighted -= 1
- if (this.highlighted < 0) {
- this.highlighted = this.candidates.length - 1
- }
- } else {
- this.highlighted = 0
- }
- },
- cycleForward (e) {
- const len = this.candidates.length || 0
- if (len > 0) {
- if (e.shiftKey) { return }
- e.preventDefault()
- this.highlighted += 1
- if (this.highlighted >= len) {
- this.highlighted = 0
- }
- } else {
- this.highlighted = 0
- }
- },
- onKeydown (e) {
- e.stopPropagation()
- },
- setCaret ({target: {selectionStart}}) {
- this.caret = selectionStart
- },
postStatus (newStatus) {
if (this.posting) { return }
if (this.submitDisabled) { return }
@@ -252,6 +194,12 @@ const PostStatusForm = {
}
}
+ const poll = this.pollFormVisible ? this.newStatus.poll : {}
+ if (this.pollContentError) {
+ this.error = this.pollContentError
+ return
+ }
+
this.posting = true
statusPoster.postStatus({
status: newStatus.status,
@@ -261,7 +209,8 @@ const PostStatusForm = {
media: newStatus.files,
store: this.$store,
inReplyToStatusId: this.replyTo,
- contentType: newStatus.contentType
+ contentType: newStatus.contentType,
+ poll
}).then((data) => {
if (!data.error) {
this.newStatus = {
@@ -269,9 +218,13 @@ const PostStatusForm = {
spoilerText: '',
files: [],
visibility: newStatus.visibility,
- contentType: newStatus.contentType
+ contentType: newStatus.contentType,
+ poll: {}
}
+ this.pollFormVisible = false
+ this.stickerPickerVisible = false
this.$refs.mediaUpload.clearFile()
+ this.clearPollForm()
this.$emit('posted')
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
@@ -286,6 +239,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
this.enableSubmit()
+ this.stickerPickerVisible = false
},
removeMediaFile (fileInfo) {
let index = this.newStatus.files.indexOf(fileInfo)
@@ -317,7 +271,7 @@ const PostStatusForm = {
},
fileDrop (e) {
if (e.dataTransfer.files.length > 0) {
- e.preventDefault() // allow dropping text like before
+ e.preventDefault() // allow dropping text like before
this.dropFiles = e.dataTransfer.files
}
},
@@ -327,8 +281,11 @@ const PostStatusForm = {
resize (e) {
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))
+ const topPaddingStr = window.getComputedStyle(target)['padding-top']
+ const bottomPaddingStr = window.getComputedStyle(target)['padding-bottom']
+ // Remove "px" at the end of the values
+ const vertPadding = Number(topPaddingStr.substr(0, topPaddingStr.length - 2)) +
+ Number(bottomPaddingStr.substr(0, bottomPaddingStr.length - 2))
// Auto is needed to make textbox shrink when removing lines
target.style.height = 'auto'
target.style.height = `${target.scrollHeight - vertPadding}px`
@@ -342,6 +299,25 @@ const PostStatusForm = {
changeVis (visibility) {
this.newStatus.visibility = visibility
},
+ toggleStickerPicker () {
+ this.stickerPickerVisible = !this.stickerPickerVisible
+ },
+ clearStickerPicker () {
+ if (this.$refs.stickerPicker) {
+ this.$refs.stickerPicker.clear()
+ }
+ },
+ togglePollForm () {
+ this.pollFormVisible = !this.pollFormVisible
+ },
+ setPoll (poll) {
+ this.newStatus.poll = poll
+ },
+ clearPollForm () {
+ if (this.$refs.pollForm) {
+ this.$refs.pollForm.clear()
+ }
+ },
dismissScopeNotice () {
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
}
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 25c5284f..d29d47e4 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -1,127 +1,268 @@
<template>
-<div class="post-status-form">
- <form @submit.prevent="postStatus(newStatus)">
- <div class="form-group" >
- <i18n
- v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
- path="post_status.account_not_locked_warning"
- tag="p"
- class="visibility-notice">
- <router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
- </i18n>
- <p v-if="!hideScopeNotice && newStatus.visibility === 'public'" class="visibility-notice notice-dismissible">
- <span>{{ $t('post_status.scope_notice.public') }}</span>
- <a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
- <i class='icon-cancel'></i>
- </a>
- </p>
- <p v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'" class="visibility-notice notice-dismissible">
- <span>{{ $t('post_status.scope_notice.unlisted') }}</span>
- <a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
- <i class='icon-cancel'></i>
- </a>
- </p>
- <p v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked" class="visibility-notice notice-dismissible">
- <span>{{ $t('post_status.scope_notice.private') }}</span>
- <a v-on:click.prevent="dismissScopeNotice()" class="button-icon dismiss">
- <i class='icon-cancel'></i>
- </a>
- </p>
- <p v-else-if="newStatus.visibility === 'direct'" class="visibility-notice">
- <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
- <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
- </p>
- <EmojiInput
- v-if="newStatus.spoilerText || alwaysShowSubject"
- type="text"
- :placeholder="$t('post_status.content_warning')"
- v-model="newStatus.spoilerText"
- classname="form-control"
- />
- <textarea
- ref="textarea"
- @click="setCaret"
- @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
- @keydown="onKeydown"
- @keydown.down="cycleForward"
- @keydown.up="cycleBackward"
- @keydown.shift.tab="cycleBackward"
- @keydown.tab="cycleForward"
- @keydown.enter="replaceCandidate"
- @keydown.meta.enter="postStatus(newStatus)"
- @keyup.ctrl.enter="postStatus(newStatus)"
- @drop="fileDrop"
- @dragover.prevent="fileDrag"
- @input="resize"
- @paste="paste"
- :disabled="posting"
- >
- </textarea>
- <div class="visibility-tray">
- <div class="text-format" v-if="formattingOptionsEnabled">
- <label for="post-content-type" class="select">
- <select id="post-content-type" v-model="newStatus.contentType" class="form-control">
- <option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
- {{$t(`post_status.content_type["${postFormat}"]`)}}
- </option>
- </select>
- <i class="icon-down-open"></i>
- </label>
- </div>
+ <div class="post-status-form">
+ <form
+ autocomplete="off"
+ @submit.prevent="postStatus(newStatus)"
+ >
+ <div class="form-group">
+ <i18n
+ v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
+ path="post_status.account_not_locked_warning"
+ tag="p"
+ class="visibility-notice"
+ >
+ <router-link :to="{ name: 'user-settings' }">
+ {{ $t('post_status.account_not_locked_warning_link') }}
+ </router-link>
+ </i18n>
+ <p
+ v-if="!hideScopeNotice && newStatus.visibility === 'public'"
+ class="visibility-notice notice-dismissible"
+ >
+ <span>{{ $t('post_status.scope_notice.public') }}</span>
+ <a
+ class="button-icon dismiss"
+ @click.prevent="dismissScopeNotice()"
+ >
+ <i class="icon-cancel" />
+ </a>
+ </p>
+ <p
+ v-else-if="!hideScopeNotice && newStatus.visibility === 'unlisted'"
+ class="visibility-notice notice-dismissible"
+ >
+ <span>{{ $t('post_status.scope_notice.unlisted') }}</span>
+ <a
+ class="button-icon dismiss"
+ @click.prevent="dismissScopeNotice()"
+ >
+ <i class="icon-cancel" />
+ </a>
+ </p>
+ <p
+ v-else-if="!hideScopeNotice && newStatus.visibility === 'private' && $store.state.users.currentUser.locked"
+ class="visibility-notice notice-dismissible"
+ >
+ <span>{{ $t('post_status.scope_notice.private') }}</span>
+ <a
+ class="button-icon dismiss"
+ @click.prevent="dismissScopeNotice()"
+ >
+ <i class="icon-cancel" />
+ </a>
+ </p>
+ <p
+ v-else-if="newStatus.visibility === 'direct'"
+ class="visibility-notice"
+ >
+ <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
+ <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
+ </p>
+ <EmojiInput
+ v-if="newStatus.spoilerText || alwaysShowSubject"
+ v-model="newStatus.spoilerText"
+ :suggest="emojiSuggestor"
+ class="form-control"
+ >
+ <input
- <scope-selector
- :showAll="showAllScopes"
- :userDefault="userDefaultScope"
- :originalScope="copyMessageScope"
- :initialScope="newStatus.visibility"
- :onScopeChange="changeVis"/>
- </div>
- </div>
- <div class="autocomplete-panel" v-if="candidates">
- <div class="autocomplete-panel-body">
+ v-model="newStatus.spoilerText"
+ type="text"
+ :placeholder="$t('post_status.content_warning')"
+ class="form-post-subject"
+ >
+ </EmojiInput>
+ <EmojiInput
+ v-model="newStatus.status"
+ :suggest="emojiUserSuggestor"
+ class="form-control main-input"
+ >
+ <textarea
+ ref="textarea"
+ v-model="newStatus.status"
+ :placeholder="$t('post_status.default')"
+ rows="1"
+ :disabled="posting"
+ class="form-post-body"
+ @keydown.meta.enter="postStatus(newStatus)"
+ @keyup.ctrl.enter="postStatus(newStatus)"
+ @drop="fileDrop"
+ @dragover.prevent="fileDrag"
+ @input="resize"
+ @paste="paste"
+ />
+ <p
+ v-if="hasStatusLengthLimit"
+ class="character-counter faint"
+ :class="{ error: isOverLengthLimit }"
+ >
+ {{ charactersLeft }}
+ </p>
+ </EmojiInput>
+ <div class="visibility-tray">
+ <scope-selector
+ :show-all="showAllScopes"
+ :user-default="userDefaultScope"
+ :original-scope="copyMessageScope"
+ :initial-scope="newStatus.visibility"
+ :on-scope-change="changeVis"
+ />
+
+ <div
+ v-if="postFormats.length > 1"
+ class="text-format"
+ >
+ <label
+ for="post-content-type"
+ class="select"
+ >
+ <select
+ id="post-content-type"
+ v-model="newStatus.contentType"
+ class="form-control"
+ >
+ <option
+ v-for="postFormat in postFormats"
+ :key="postFormat"
+ :value="postFormat"
+ >
+ {{ $t(`post_status.content_type["${postFormat}"]`) }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
<div
- v-for="(candidate, index) in candidates"
- :key="index"
- @click="replace(candidate.utf || (candidate.screen_name + ' '))"
- class="autocomplete-item"
- :class="{ highlighted: candidate.highlighted }"
+ v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'"
+ class="text-format"
>
- <span v-if="candidate.img"><img :src="candidate.img" /></span>
- <span v-else>{{candidate.utf}}</span>
- <span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
+ <span class="only-format">
+ {{ $t(`post_status.content_type["${postFormats[0]}"]`) }}
+ </span>
</div>
</div>
</div>
- <div class='form-bottom'>
- <media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
-
- <p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
- <p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
-
- <button v-if="posting" disabled class="btn btn-default">{{$t('post_status.posting')}}</button>
- <button v-else-if="isOverLengthLimit" disabled class="btn btn-default">{{$t('general.submit')}}</button>
- <button v-else :disabled="submitDisabled" type="submit" class="btn btn-default">{{$t('general.submit')}}</button>
+ <poll-form
+ v-if="pollsAvailable"
+ ref="pollForm"
+ :visible="pollFormVisible"
+ @update-poll="setPoll"
+ />
+ <div class="form-bottom">
+ <div class="form-bottom-left">
+ <media-upload
+ ref="mediaUpload"
+ :drop-files="dropFiles"
+ @uploading="disableSubmit"
+ @uploaded="addMediaFile"
+ @upload-failed="uploadFailed"
+ />
+ <div
+ v-if="stickersAvailable"
+ class="sticker-icon"
+ >
+ <i
+ :title="$t('stickers.add_sticker')"
+ class="icon-picture btn btn-default"
+ :class="{ selected: stickerPickerVisible }"
+ @click="toggleStickerPicker"
+ />
+ </div>
+ <div
+ v-if="pollsAvailable"
+ class="poll-icon"
+ >
+ <i
+ :title="$t('polls.add_poll')"
+ class="icon-chart-bar btn btn-default"
+ :class="pollFormVisible && 'selected'"
+ @click="togglePollForm"
+ />
+ </div>
+ </div>
+ <button
+ v-if="posting"
+ disabled
+ class="btn btn-default"
+ >
+ {{ $t('post_status.posting') }}
+ </button>
+ <button
+ v-else-if="isOverLengthLimit"
+ disabled
+ class="btn btn-default"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <button
+ v-else
+ :disabled="submitDisabled"
+ type="submit"
+ class="btn btn-default"
+ >
+ {{ $t('general.submit') }}
+ </button>
</div>
- <div class='alert error' v-if="error">
+ <div
+ v-if="error"
+ class="alert error"
+ >
Error: {{ error }}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <i
+ class="button-icon icon-cancel"
+ @click="clearError"
+ />
</div>
<div class="attachments">
- <div class="media-upload-wrapper" v-for="file in newStatus.files">
- <i class="fa button-icon icon-cancel" @click="removeMediaFile(file)"></i>
+ <div
+ v-for="file in newStatus.files"
+ :key="file.url"
+ class="media-upload-wrapper"
+ >
+ <i
+ class="fa button-icon icon-cancel"
+ @click="removeMediaFile(file)"
+ />
<div class="media-upload-container attachment">
- <img class="thumbnail media-upload" :src="file.url" v-if="type(file) === 'image'"></img>
- <video v-if="type(file) === 'video'" :src="file.url" controls></video>
- <audio v-if="type(file) === 'audio'" :src="file.url" controls></audio>
- <a v-if="type(file) === 'unknown'" :href="file.url">{{file.url}}</a>
+ <img
+ v-if="type(file) === 'image'"
+ class="thumbnail media-upload"
+ :src="file.url"
+ >
+ <video
+ v-if="type(file) === 'video'"
+ :src="file.url"
+ controls
+ />
+ <audio
+ v-if="type(file) === 'audio'"
+ :src="file.url"
+ controls
+ />
+ <a
+ v-if="type(file) === 'unknown'"
+ :href="file.url"
+ >{{ file.url }}</a>
</div>
</div>
</div>
- <div class="upload_settings" v-if="newStatus.files.length > 0">
- <input type="checkbox" id="filesSensitive" v-model="newStatus.nsfw">
- <label for="filesSensitive">{{$t('post_status.attachments_sensitive')}}</label>
+ <div
+ v-if="newStatus.files.length > 0"
+ class="upload_settings"
+ >
+ <input
+ id="filesSensitive"
+ v-model="newStatus.nsfw"
+ type="checkbox"
+ >
+ <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
</div>
</form>
+ <sticker-picker
+ v-if="stickerPickerVisible"
+ ref="stickerPicker"
+ @uploaded="addMediaFile"
+ />
</div>
</template>
@@ -151,7 +292,6 @@
.visibility-tray {
display: flex;
justify-content: space-between;
- flex-direction: row-reverse;
padding-top: 5px;
}
}
@@ -173,6 +313,37 @@
}
}
+ .form-bottom-left {
+ display: flex;
+ flex: 1;
+ }
+
+ .text-format {
+ .only-format {
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ }
+ }
+
+ .poll-icon, .sticker-icon {
+ font-size: 26px;
+ flex: 1;
+
+ .selected {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ }
+ }
+
+ .sticker-icon {
+ flex: 0;
+ min-width: 50px;
+ }
+
+ .icon-chart-bar {
+ cursor: pointer;
+ }
+
.error {
text-align: center;
}
@@ -233,7 +404,6 @@
}
}
-
.btn {
cursor: pointer;
}
@@ -263,19 +433,38 @@
min-height: 1px;
}
- form textarea.form-control {
- line-height:16px;
+ .form-post-body {
+ height: 16px; // Only affects the empty-height
+ line-height: 16px;
resize: none;
overflow: hidden;
transition: min-height 200ms 100ms;
+ padding-bottom: 1.75em;
min-height: 1px;
box-sizing: content-box;
}
- form textarea.form-control:focus {
+ .form-post-body:focus {
min-height: 48px;
}
+ .main-input {
+ position: relative;
+ }
+
+ .character-counter {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ padding: 0;
+ margin: 0 0.5em;
+
+ &.error {
+ color: $fallback--cRed;
+ color: var(--cRed, $fallback--cRed);
+ }
+ }
+
.btn {
cursor: pointer;
}
diff --git a/src/components/progress_button/progress_button.vue b/src/components/progress_button/progress_button.vue
index 737360bb..283a51af 100644
--- a/src/components/progress_button/progress_button.vue
+++ b/src/components/progress_button/progress_button.vue
@@ -1,6 +1,9 @@
<template>
- <button :disabled="progress || disabled" @click="onClick">
- <template v-if="progress">
+ <button
+ :disabled="progress || disabled"
+ @click="onClick"
+ >
+ <template v-if="progress && $slots.progress">
<slot name="progress" />
</template>
<template v-else>
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.vue b/src/components/public_and_external_timeline/public_and_external_timeline.vue
index 6be9f955..fcd915ac 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.vue
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.vue
@@ -1,5 +1,9 @@
<template>
- <Timeline :title="$t('nav.twkn')" v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
+ <Timeline
+ :title="$t('nav.twkn')"
+ :timeline="timeline"
+ :timeline-name="'publicAndExternal'"
+ />
</template>
<script src="./public_and_external_timeline.js"></script>
diff --git a/src/components/public_timeline/public_timeline.vue b/src/components/public_timeline/public_timeline.vue
index 85d42cca..5720068d 100644
--- a/src/components/public_timeline/public_timeline.vue
+++ b/src/components/public_timeline/public_timeline.vue
@@ -1,5 +1,9 @@
<template>
- <Timeline :title="$t('nav.public_tl')" v-bind:timeline="timeline" v-bind:timeline-name="'public'"/>
+ <Timeline
+ :title="$t('nav.public_tl')"
+ :timeline="timeline"
+ :timeline-name="'public'"
+ />
</template>
<script src="./public_timeline.js"></script>
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
index 3e50664b..aaa2ed26 100644
--- a/src/components/range_input/range_input.vue
+++ b/src/components/range_input/range_input.vue
@@ -1,37 +1,50 @@
<template>
-<div class="range-control style-control" :class="{ disabled: !present || disabled }">
- <label :for="name" class="label">
- {{label}}
- </label>
- <input
- v-if="typeof fallback !== 'undefined'"
- class="opt exclude-disabled"
- :id="name + '-o'"
- type="checkbox"
- :checked="present"
- @input="$emit('input', !present ? fallback : undefined)">
- <label v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'"></label>
- <input
- :id="name"
- class="input-number"
- type="range"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
- :max="max || hardMax || 100"
- :min="min || hardMin || 0"
- :step="step || 1">
- <input
- :id="name"
- class="input-number"
- type="number"
- :value="value || fallback"
- :disabled="!present || disabled"
- @input="$emit('input', $event.target.value)"
- :max="hardMax"
- :min="hardMin"
- :step="step || 1">
-</div>
+ <div
+ class="range-control style-control"
+ :class="{ disabled: !present || disabled }"
+ >
+ <label
+ :for="name"
+ class="label"
+ >
+ {{ label }}
+ </label>
+ <input
+ v-if="typeof fallback !== 'undefined'"
+ :id="name + '-o'"
+ class="opt exclude-disabled"
+ type="checkbox"
+ :checked="present"
+ @input="$emit('input', !present ? fallback : undefined)"
+ >
+ <label
+ v-if="typeof fallback !== 'undefined'"
+ class="opt-l"
+ :for="name + '-o'"
+ />
+ <input
+ :id="name"
+ class="input-number"
+ type="range"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ :max="max || hardMax || 100"
+ :min="min || hardMin || 0"
+ :step="step || 1"
+ @input="$emit('input', $event.target.value)"
+ >
+ <input
+ :id="name"
+ class="input-number"
+ type="number"
+ :value="value || fallback"
+ :disabled="!present || disabled"
+ :max="hardMax"
+ :min="hardMin"
+ :step="step || 1"
+ @input="$emit('input', $event.target.value)"
+ >
+ </div>
</template>
<script>
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 8dc00420..57f3caf0 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -28,7 +28,7 @@ const registration = {
},
created () {
if ((!this.registrationOpen && !this.token) || this.signedIn) {
- this.$router.push({name: 'root'})
+ this.$router.push({ name: 'root' })
}
this.setCaptcha()
@@ -61,7 +61,7 @@ const registration = {
if (!this.$v.$invalid) {
try {
await this.signUp(this.user)
- this.$router.push({name: 'friends'})
+ this.$router.push({ name: 'friends' })
} catch (error) {
console.warn('Registration failed: ' + error)
}
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 110b27bf..5bb06a4f 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -1,109 +1,236 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
- {{$t('registration.registration')}}
+ {{ $t('registration.registration') }}
</div>
<div class="panel-body">
- <form v-on:submit.prevent='submit(user)' class='registration-form'>
- <div class='container'>
- <div class='text-fields'>
- <div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
- <label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
- <input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' :placeholder="$t('registration.username_placeholder')">
+ <form
+ class="registration-form"
+ @submit.prevent="submit(user)"
+ >
+ <div class="container">
+ <div class="text-fields">
+ <div
+ class="form-group"
+ :class="{ 'form-group--error': $v.user.username.$error }"
+ >
+ <label
+ class="form--label"
+ for="sign-up-username"
+ >{{ $t('login.username') }}</label>
+ <input
+ id="sign-up-username"
+ v-model.trim="$v.user.username.$model"
+ :disabled="isPending"
+ class="form-control"
+ :placeholder="$t('registration.username_placeholder')"
+ >
</div>
- <div class="form-error" v-if="$v.user.username.$dirty">
+ <div
+ v-if="$v.user.username.$dirty"
+ class="form-error"
+ >
<ul>
<li v-if="!$v.user.username.required">
- <span>{{$t('registration.validations.username_required')}}</span>
+ <span>{{ $t('registration.validations.username_required') }}</span>
</li>
</ul>
</div>
- <div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
- <label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
- <input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' :placeholder="$t('registration.fullname_placeholder')">
+ <div
+ class="form-group"
+ :class="{ 'form-group--error': $v.user.fullname.$error }"
+ >
+ <label
+ class="form--label"
+ for="sign-up-fullname"
+ >{{ $t('registration.fullname') }}</label>
+ <input
+ id="sign-up-fullname"
+ v-model.trim="$v.user.fullname.$model"
+ :disabled="isPending"
+ class="form-control"
+ :placeholder="$t('registration.fullname_placeholder')"
+ >
</div>
- <div class="form-error" v-if="$v.user.fullname.$dirty">
+ <div
+ v-if="$v.user.fullname.$dirty"
+ class="form-error"
+ >
<ul>
<li v-if="!$v.user.fullname.required">
- <span>{{$t('registration.validations.fullname_required')}}</span>
+ <span>{{ $t('registration.validations.fullname_required') }}</span>
</li>
</ul>
</div>
- <div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
- <label class='form--label' for='email'>{{$t('registration.email')}}</label>
- <input :disabled="isPending" v-model='$v.user.email.$model' class='form-control' id='email' type="email">
+ <div
+ class="form-group"
+ :class="{ 'form-group--error': $v.user.email.$error }"
+ >
+ <label
+ class="form--label"
+ for="email"
+ >{{ $t('registration.email') }}</label>
+ <input
+ id="email"
+ v-model="$v.user.email.$model"
+ :disabled="isPending"
+ class="form-control"
+ type="email"
+ >
</div>
- <div class="form-error" v-if="$v.user.email.$dirty">
+ <div
+ v-if="$v.user.email.$dirty"
+ class="form-error"
+ >
<ul>
<li v-if="!$v.user.email.required">
- <span>{{$t('registration.validations.email_required')}}</span>
+ <span>{{ $t('registration.validations.email_required') }}</span>
</li>
</ul>
</div>
- <div class='form-group'>
- <label class='form--label' for='bio'>{{$t('registration.bio')}} ({{$t('general.optional')}})</label>
- <textarea :disabled="isPending" v-model='user.bio' class='form-control' id='bio' :placeholder="bioPlaceholder"></textarea>
+ <div class="form-group">
+ <label
+ class="form--label"
+ for="bio"
+ >{{ $t('registration.bio') }} ({{ $t('general.optional') }})</label>
+ <textarea
+ id="bio"
+ v-model="user.bio"
+ :disabled="isPending"
+ class="form-control"
+ :placeholder="bioPlaceholder"
+ />
</div>
- <div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
- <label class='form--label' for='sign-up-password'>{{$t('login.password')}}</label>
- <input :disabled="isPending" v-model='user.password' class='form-control' id='sign-up-password' type='password'>
+ <div
+ class="form-group"
+ :class="{ 'form-group--error': $v.user.password.$error }"
+ >
+ <label
+ class="form--label"
+ for="sign-up-password"
+ >{{ $t('login.password') }}</label>
+ <input
+ id="sign-up-password"
+ v-model="user.password"
+ :disabled="isPending"
+ class="form-control"
+ type="password"
+ >
</div>
- <div class="form-error" v-if="$v.user.password.$dirty">
+ <div
+ v-if="$v.user.password.$dirty"
+ class="form-error"
+ >
<ul>
<li v-if="!$v.user.password.required">
- <span>{{$t('registration.validations.password_required')}}</span>
+ <span>{{ $t('registration.validations.password_required') }}</span>
</li>
</ul>
</div>
- <div class='form-group' :class="{ 'form-group--error': $v.user.confirm.$error }">
- <label class='form--label' for='sign-up-password-confirmation'>{{$t('registration.password_confirm')}}</label>
- <input :disabled="isPending" v-model='user.confirm' class='form-control' id='sign-up-password-confirmation' type='password'>
+ <div
+ class="form-group"
+ :class="{ 'form-group--error': $v.user.confirm.$error }"
+ >
+ <label
+ class="form--label"
+ for="sign-up-password-confirmation"
+ >{{ $t('registration.password_confirm') }}</label>
+ <input
+ id="sign-up-password-confirmation"
+ v-model="user.confirm"
+ :disabled="isPending"
+ class="form-control"
+ type="password"
+ >
</div>
- <div class="form-error" v-if="$v.user.confirm.$dirty">
+ <div
+ v-if="$v.user.confirm.$dirty"
+ class="form-error"
+ >
<ul>
<li v-if="!$v.user.confirm.required">
- <span>{{$t('registration.validations.password_confirmation_required')}}</span>
+ <span>{{ $t('registration.validations.password_confirmation_required') }}</span>
</li>
<li v-if="!$v.user.confirm.sameAsPassword">
- <span>{{$t('registration.validations.password_confirmation_match')}}</span>
+ <span>{{ $t('registration.validations.password_confirmation_match') }}</span>
</li>
</ul>
</div>
- <div class="form-group" id="captcha-group" v-if="captcha.type != 'none'">
- <label class='form--label' for='captcha-label'>{{$t('captcha')}}</label>
+ <div
+ v-if="captcha.type != 'none'"
+ id="captcha-group"
+ class="form-group"
+ >
+ <label
+ class="form--label"
+ for="captcha-label"
+ >{{ $t('captcha') }}</label>
<template v-if="captcha.type == 'kocaptcha'">
- <img v-bind:src="captcha.url" v-on:click="setCaptcha">
+ <img
+ :src="captcha.url"
+ @click="setCaptcha"
+ >
- <sub>{{$t('registration.new_captcha')}}</sub>
+ <sub>{{ $t('registration.new_captcha') }}</sub>
- <input :disabled="isPending"
- v-model='captcha.solution'
- class='form-control' id='captcha-answer' type='text' autocomplete="off">
+ <input
+ id="captcha-answer"
+ v-model="captcha.solution"
+ :disabled="isPending"
+ class="form-control"
+ type="text"
+ autocomplete="off"
+ >
</template>
</div>
- <div class='form-group' v-if='token' >
- <label for='token'>{{$t('registration.token')}}</label>
- <input disabled='true' v-model='token' class='form-control' id='token' type='text'>
+ <div
+ v-if="token"
+ class="form-group"
+ >
+ <label for="token">{{ $t('registration.token') }}</label>
+ <input
+ id="token"
+ v-model="token"
+ disabled="true"
+ class="form-control"
+ type="text"
+ >
</div>
- <div class='form-group'>
- <button :disabled="isPending" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
+ <div class="form-group">
+ <button
+ :disabled="isPending"
+ type="submit"
+ class="btn btn-default"
+ >
+ {{ $t('general.submit') }}
+ </button>
</div>
</div>
- <div class='terms-of-service' v-html="termsOfService">
- </div>
+ <!-- eslint-disable vue/no-v-html -->
+ <div
+ class="terms-of-service"
+ v-html="termsOfService"
+ />
+ <!-- eslint-enable vue/no-v-html -->
</div>
- <div v-if="serverValidationErrors.length" class='form-group'>
- <div class='alert error'>
- <span v-for="error in serverValidationErrors">{{error}}</span>
+ <div
+ v-if="serverValidationErrors.length"
+ class="form-group"
+ >
+ <div class="alert error">
+ <span
+ v-for="error in serverValidationErrors"
+ :key="error"
+ >{{ error }}</span>
</div>
</div>
</form>
@@ -141,6 +268,7 @@ $validations-cRed: #f04124;
textarea {
min-height: 100px;
+ resize: vertical;
}
.form-group {
diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue
index fb2147bd..cb1c2a1b 100644
--- a/src/components/remote_follow/remote_follow.vue
+++ b/src/components/remote_follow/remote_follow.vue
@@ -1,9 +1,23 @@
<template>
<div class="remote-follow">
- <form method="POST" :action='subscribeUrl'>
- <input type="hidden" name="nickname" :value="user.screen_name">
- <input type="hidden" name="profile" value="">
- <button click="submit" class="remote-button">
+ <form
+ method="POST"
+ :action="subscribeUrl"
+ >
+ <input
+ type="hidden"
+ name="nickname"
+ :value="user.screen_name"
+ >
+ <input
+ type="hidden"
+ name="profile"
+ value=""
+ >
+ <button
+ click="submit"
+ class="remote-button"
+ >
{{ $t('user_card.remote_follow') }}
</button>
</form>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index eb4e4b41..fb543a9c 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -11,9 +11,9 @@ const RetweetButton = {
methods: {
retweet () {
if (!this.status.repeated) {
- this.$store.dispatch('retweet', {id: this.status.id})
+ this.$store.dispatch('retweet', { id: this.status.id })
} else {
- this.$store.dispatch('unretweet', {id: this.status.id})
+ this.$store.dispatch('unretweet', { id: this.status.id })
}
this.animated = true
setTimeout(() => {
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 6370f9dc..d58a7f8c 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,16 +1,29 @@
<template>
<div v-if="loggedIn">
<template v-if="visibility !== 'private' && visibility !== 'direct'">
- <i :class='classes' class='button-icon retweet-button icon-retweet rt-active' v-on:click.prevent='retweet()' :title="$t('tool_tip.repeat')"></i>
- <span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span>
+ <i
+ :class="classes"
+ class="button-icon retweet-button icon-retweet rt-active"
+ :title="$t('tool_tip.repeat')"
+ @click.prevent="retweet()"
+ />
+ <span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span>
</template>
<template v-else>
- <i :class='classes' class='button-icon icon-lock' :title="$t('timeline.no_retweet_hint')"></i>
+ <i
+ :class="classes"
+ class="button-icon icon-lock"
+ :title="$t('timeline.no_retweet_hint')"
+ />
</template>
</div>
<div v-else-if="!loggedIn">
- <i :class='classes' class='button-icon icon-retweet' :title="$t('tool_tip.repeat')"></i>
- <span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span>
+ <i
+ :class="classes"
+ class="button-icon icon-retweet"
+ :title="$t('tool_tip.repeat')"
+ />
+ <span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span>
</div>
</template>
diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js
index 8a42ee7b..e9ccdefc 100644
--- a/src/components/scope_selector/scope_selector.js
+++ b/src/components/scope_selector/scope_selector.js
@@ -29,10 +29,10 @@ const ScopeSelector = {
},
css () {
return {
- public: {selected: this.currentScope === 'public'},
- unlisted: {selected: this.currentScope === 'unlisted'},
- private: {selected: this.currentScope === 'private'},
- direct: {selected: this.currentScope === 'direct'}
+ public: { selected: this.currentScope === 'public' },
+ unlisted: { selected: this.currentScope === 'unlisted' },
+ private: { selected: this.currentScope === 'private' },
+ direct: { selected: this.currentScope === 'direct' }
}
}
},
diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue
index 5ebb5d56..291236f2 100644
--- a/src/components/scope_selector/scope_selector.vue
+++ b/src/components/scope_selector/scope_selector.vue
@@ -1,30 +1,37 @@
<template>
-<div v-if="!showNothing" class="scope-selector">
- <i class="icon-mail-alt"
- :class="css.direct"
- :title="$t('post_status.scope.direct')"
- v-if="showDirect"
- @click="changeVis('direct')">
- </i>
- <i class="icon-lock"
- :class="css.private"
- :title="$t('post_status.scope.private')"
- v-if="showPrivate"
- v-on:click="changeVis('private')">
- </i>
- <i class="icon-lock-open-alt"
- :class="css.unlisted"
- :title="$t('post_status.scope.unlisted')"
- v-if="showUnlisted"
- @click="changeVis('unlisted')">
- </i>
- <i class="icon-globe"
- :class="css.public"
- :title="$t('post_status.scope.public')"
- v-if="showPublic"
- @click="changeVis('public')">
- </i>
-</div>
+ <div
+ v-if="!showNothing"
+ class="scope-selector"
+ >
+ <i
+ v-if="showDirect"
+ class="icon-mail-alt"
+ :class="css.direct"
+ :title="$t('post_status.scope.direct')"
+ @click="changeVis('direct')"
+ />
+ <i
+ v-if="showPrivate"
+ class="icon-lock"
+ :class="css.private"
+ :title="$t('post_status.scope.private')"
+ @click="changeVis('private')"
+ />
+ <i
+ v-if="showUnlisted"
+ class="icon-lock-open-alt"
+ :class="css.unlisted"
+ :title="$t('post_status.scope.unlisted')"
+ @click="changeVis('unlisted')"
+ />
+ <i
+ v-if="showPublic"
+ class="icon-globe"
+ :class="css.public"
+ :title="$t('post_status.scope.public')"
+ @click="changeVis('public')"
+ />
+ </div>
</template>
<script src="./scope_selector.js"></script>
diff --git a/src/components/search/search.js b/src/components/search/search.js
new file mode 100644
index 00000000..8e903052
--- /dev/null
+++ b/src/components/search/search.js
@@ -0,0 +1,98 @@
+import FollowCard from '../follow_card/follow_card.vue'
+import Conversation from '../conversation/conversation.vue'
+import Status from '../status/status.vue'
+import map from 'lodash/map'
+
+const Search = {
+ components: {
+ FollowCard,
+ Conversation,
+ Status
+ },
+ props: [
+ 'query'
+ ],
+ data () {
+ return {
+ loaded: false,
+ loading: false,
+ searchTerm: this.query || '',
+ userIds: [],
+ statuses: [],
+ hashtags: [],
+ currenResultTab: 'statuses'
+ }
+ },
+ computed: {
+ users () {
+ return this.userIds.map(userId => this.$store.getters.findUser(userId))
+ },
+ visibleStatuses () {
+ const allStatusesObject = this.$store.state.statuses.allStatusesObject
+
+ return this.statuses.filter(status =>
+ allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
+ )
+ }
+ },
+ mounted () {
+ this.search(this.query)
+ },
+ watch: {
+ query (newValue) {
+ this.searchTerm = newValue
+ this.search(newValue)
+ }
+ },
+ methods: {
+ newQuery (query) {
+ this.$router.push({ name: 'search', query: { query } })
+ this.$refs.searchInput.focus()
+ },
+ search (query) {
+ if (!query) {
+ this.loading = false
+ return
+ }
+
+ this.loading = true
+ this.userIds = []
+ this.statuses = []
+ this.hashtags = []
+ this.$refs.searchInput.blur()
+
+ this.$store.dispatch('search', { q: query, resolve: true })
+ .then(data => {
+ this.loading = false
+ this.userIds = map(data.accounts, 'id')
+ this.statuses = data.statuses
+ this.hashtags = data.hashtags
+ this.currenResultTab = this.getActiveTab()
+ this.loaded = true
+ })
+ },
+ resultCount (tabName) {
+ const length = this[tabName].length
+ return length === 0 ? '' : ` (${length})`
+ },
+ onResultTabSwitch (key) {
+ this.currenResultTab = key
+ },
+ getActiveTab () {
+ if (this.visibleStatuses.length > 0) {
+ return 'statuses'
+ } else if (this.users.length > 0) {
+ return 'people'
+ } else if (this.hashtags.length > 0) {
+ return 'hashtags'
+ }
+
+ return 'statuses'
+ },
+ lastHistoryRecord (hashtag) {
+ return hashtag.history && hashtag.history[0]
+ }
+ }
+}
+
+export default Search
diff --git a/src/components/search/search.vue b/src/components/search/search.vue
new file mode 100644
index 00000000..746bbaa2
--- /dev/null
+++ b/src/components/search/search.vue
@@ -0,0 +1,208 @@
+<template>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <div class="title">
+ {{ $t('nav.search') }}
+ </div>
+ </div>
+ <div class="search-input-container">
+ <input
+ ref="searchInput"
+ v-model="searchTerm"
+ class="search-input"
+ :placeholder="$t('nav.search')"
+ @keyup.enter="newQuery(searchTerm)"
+ >
+ <button
+ class="btn search-button"
+ @click="newQuery(searchTerm)"
+ >
+ <i class="icon-search" />
+ </button>
+ </div>
+ <div
+ v-if="loading"
+ class="text-center loading-icon"
+ >
+ <i class="icon-spin3 animate-spin" />
+ </div>
+ <div v-else-if="loaded">
+ <div class="search-nav-heading">
+ <tab-switcher
+ ref="tabSwitcher"
+ :on-switch="onResultTabSwitch"
+ :active-tab="currenResultTab"
+ >
+ <span
+ key="statuses"
+ :label="$t('user_card.statuses') + resultCount('visibleStatuses')"
+ />
+ <span
+ key="people"
+ :label="$t('search.people') + resultCount('users')"
+ />
+ <span
+ key="hashtags"
+ :label="$t('search.hashtags') + resultCount('hashtags')"
+ />
+ </tab-switcher>
+ </div>
+ </div>
+ <div class="panel-body">
+ <div v-if="currenResultTab === 'statuses'">
+ <div
+ v-if="visibleStatuses.length === 0 && !loading && loaded"
+ class="search-result-heading"
+ >
+ <h4>{{ $t('search.no_results') }}</h4>
+ </div>
+ <Status
+ v-for="status in visibleStatuses"
+ :key="status.id"
+ :collapsable="false"
+ :expandable="false"
+ :compact="false"
+ class="search-result"
+ :statusoid="status"
+ :no-heading="false"
+ />
+ </div>
+ <div v-else-if="currenResultTab === 'people'">
+ <div
+ v-if="users.length === 0 && !loading && loaded"
+ class="search-result-heading"
+ >
+ <h4>{{ $t('search.no_results') }}</h4>
+ </div>
+ <FollowCard
+ v-for="user in users"
+ :key="user.id"
+ :user="user"
+ class="list-item search-result"
+ />
+ </div>
+ <div v-else-if="currenResultTab === 'hashtags'">
+ <div
+ v-if="hashtags.length === 0 && !loading && loaded"
+ class="search-result-heading"
+ >
+ <h4>{{ $t('search.no_results') }}</h4>
+ </div>
+ <div
+ v-for="hashtag in hashtags"
+ :key="hashtag.url"
+ class="status trend search-result"
+ >
+ <div class="hashtag">
+ <router-link :to="{ name: 'tag-timeline', params: { tag: hashtag.name } }">
+ #{{ hashtag.name }}
+ </router-link>
+ <div v-if="lastHistoryRecord(hashtag)">
+ <span v-if="lastHistoryRecord(hashtag).accounts == 1">
+ {{ $t('search.person_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
+ </span>
+ <span v-else>
+ {{ $t('search.people_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
+ </span>
+ </div>
+ </div>
+ <div
+ v-if="lastHistoryRecord(hashtag)"
+ class="count"
+ >
+ {{ lastHistoryRecord(hashtag).uses }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="search-result-footer text-center panel-footer faint" />
+ </div>
+</template>
+
+<script src="./search.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.search-result-heading {
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ padding: 0.75rem;
+ text-align: center;
+}
+
+@media all and (max-width: 800px) {
+ .search-nav-heading {
+ .tab-switcher .tabs .tab-wrapper {
+ display: block;
+ justify-content: center;
+ flex: 1 1 auto;
+ text-align: center;
+ }
+ }
+}
+
+.search-result {
+ box-sizing: border-box;
+ border-bottom: 1px solid;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+}
+
+.search-result-footer {
+ border-width: 1px 0 0 0;
+ border-style: solid;
+ border-color: var(--border, $fallback--border);
+ padding: 10px;
+ background-color: $fallback--fg;
+ background-color: var(--panel, $fallback--fg);
+}
+
+.search-input-container {
+ padding: 0.8rem;
+ display: flex;
+ justify-content: center;
+
+ .search-input {
+ width: 100%;
+ line-height: 1.125rem;
+ font-size: 1rem;
+ padding: 0.5rem;
+ box-sizing: border-box;
+ }
+
+ .search-button {
+ margin-left: 0.5em;
+ }
+}
+
+.loading-icon {
+ padding: 1em;
+}
+
+.trend {
+ display: flex;
+ align-items: center;
+
+ .hashtag {
+ flex: 1 1 auto;
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .count {
+ flex: 0 0 auto;
+ width: 2rem;
+ font-size: 1.5rem;
+ line-height: 2.25rem;
+ font-weight: 500;
+ text-align: center;
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+}
+
+</style>
diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js
new file mode 100644
index 00000000..d7d85676
--- /dev/null
+++ b/src/components/search_bar/search_bar.js
@@ -0,0 +1,32 @@
+const SearchBar = {
+ data: () => ({
+ searchTerm: undefined,
+ hidden: true,
+ error: false,
+ loading: false
+ }),
+ watch: {
+ '$route': function (route) {
+ if (route.name === 'search') {
+ this.searchTerm = route.query.query
+ }
+ }
+ },
+ methods: {
+ find (searchTerm) {
+ this.$router.push({ name: 'search', query: { query: searchTerm } })
+ this.$refs.searchInput.focus()
+ },
+ toggleHidden () {
+ this.hidden = !this.hidden
+ this.$emit('toggled', this.hidden)
+ this.$nextTick(() => {
+ if (!this.hidden) {
+ this.$refs.searchInput.focus()
+ }
+ })
+ }
+ }
+}
+
+export default SearchBar
diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue
new file mode 100644
index 00000000..4d5a1aec
--- /dev/null
+++ b/src/components/search_bar/search_bar.vue
@@ -0,0 +1,73 @@
+<template>
+ <div>
+ <div class="search-bar-container">
+ <i
+ v-if="loading"
+ class="icon-spin4 finder-icon animate-spin-slow"
+ />
+ <a
+ v-if="hidden"
+ href="#"
+ :title="$t('nav.search')"
+ ><i
+ class="button-icon icon-search"
+ @click.prevent.stop="toggleHidden"
+ /></a>
+ <template v-else>
+ <input
+ id="search-bar-input"
+ ref="searchInput"
+ v-model="searchTerm"
+ class="search-bar-input"
+ :placeholder="$t('nav.search')"
+ type="text"
+ @keyup.enter="find(searchTerm)"
+ >
+ <button
+ class="btn search-button"
+ @click="find(searchTerm)"
+ >
+ <i class="icon-search" />
+ </button>
+ <i
+ class="button-icon icon-cancel"
+ @click.prevent.stop="toggleHidden"
+ />
+ </template>
+ </div>
+ </div>
+</template>
+
+<script src="./search_bar.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.search-bar-container {
+ max-width: 100%;
+ display: inline-flex;
+ align-items: baseline;
+ vertical-align: baseline;
+ justify-content: flex-end;
+
+ .search-bar-input,
+ .search-button {
+ height: 29px;
+ }
+
+ .search-bar-input {
+ // TODO: do this properly without a rough guesstimate of 2 icons + paddings
+ max-width: calc(100% - 30px - 30px - 20px);
+ }
+
+ .search-button {
+ margin-left: .5em;
+ margin-right: .5em;
+ }
+
+ .icon-cancel {
+ cursor: pointer;
+ }
+}
+
+</style>
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
index 3f16c921..d9ec7ece 100644
--- a/src/components/selectable_list/selectable_list.vue
+++ b/src/components/selectable_list/selectable_list.vue
@@ -1,23 +1,52 @@
<template>
<div class="selectable-list">
- <div class="selectable-list-header" v-if="items.length > 0">
+ <div
+ v-if="items.length > 0"
+ class="selectable-list-header"
+ >
<div class="selectable-list-checkbox-wrapper">
- <Checkbox :checked="allSelected" @change="toggleAll" :indeterminate="someSelected">{{ $t('selectable_list.select_all') }}</Checkbox>
+ <Checkbox
+ :checked="allSelected"
+ :indeterminate="someSelected"
+ @change="toggleAll"
+ >
+ {{ $t('selectable_list.select_all') }}
+ </Checkbox>
</div>
<div class="selectable-list-header-actions">
- <slot name="header" :selected="filteredSelected" />
+ <slot
+ name="header"
+ :selected="filteredSelected"
+ />
</div>
</div>
- <List :items="items" :getKey="getKey">
- <template slot="item" slot-scope="{item}">
- <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }">
+ <List
+ :items="items"
+ :get-key="getKey"
+ >
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <div
+ class="selectable-list-item-inner"
+ :class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
+ >
<div class="selectable-list-checkbox-wrapper">
- <Checkbox :checked="isSelected(item)" @change="checked => toggle(checked, item)" />
+ <Checkbox
+ :checked="isSelected(item)"
+ @change="checked => toggle(checked, item)"
+ />
</div>
- <slot name="item" :item="item" />
+ <slot
+ name="item"
+ :item="item"
+ />
</div>
</template>
- <template slot="empty"><slot name="empty" /></template>
+ <template slot="empty">
+ <slot name="empty" />
+ </template>
</List>
</div>
</template>
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 4cf6fae2..744ec566 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -1,304 +1,482 @@
<template>
-<div class="settings panel panel-default">
- <div class="panel-heading">
- <div class="title">
- {{$t('settings.settings')}}
- </div>
+ <div class="settings panel panel-default">
+ <div class="panel-heading">
+ <div class="title">
+ {{ $t('settings.settings') }}
+ </div>
- <transition name="fade">
- <template v-if="currentSaveStateNotice">
- <div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
- {{ $t('settings.saving_err') }}
- </div>
+ <transition name="fade">
+ <template v-if="currentSaveStateNotice">
+ <div
+ v-if="currentSaveStateNotice.error"
+ class="alert error"
+ @click.prevent
+ >
+ {{ $t('settings.saving_err') }}
+ </div>
- <div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
- {{ $t('settings.saving_ok') }}
- </div>
- </template>
- </transition>
- </div>
- <div class="panel-body">
-<keep-alive>
- <tab-switcher>
- <div :label="$t('settings.general')" >
- <div class="setting-item">
- <h2>{{ $t('settings.interface') }}</h2>
- <ul class="setting-list">
- <li>
- <interface-language-switcher />
- </li>
- <li v-if="instanceSpecificPanelPresent">
- <input type="checkbox" id="hideISP" v-model="hideISPLocal">
- <label for="hideISP">{{$t('settings.hide_isp')}}</label>
- </li>
- </ul>
- </div>
- <div class="setting-item">
- <h2>{{$t('nav.timeline')}}</h2>
- <ul class="setting-list">
- <li>
- <input type="checkbox" id="hideMutedPosts" v-model="hideMutedPostsLocal">
- <label for="hideMutedPosts">{{$t('settings.hide_muted_posts')}} {{$t('settings.instance_default', { value: hideMutedPostsDefault })}}</label>
- </li>
- <li>
- <input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
- <label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}</label>
- </li>
- <li>
- <input type="checkbox" id="streaming" v-model="streamingLocal">
- <label for="streaming">{{$t('settings.streaming')}}</label>
- <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
+ <div
+ v-if="!currentSaveStateNotice.error"
+ class="alert transparent"
+ @click.prevent
+ >
+ {{ $t('settings.saving_ok') }}
+ </div>
+ </template>
+ </transition>
+ </div>
+ <div class="panel-body">
+ <keep-alive>
+ <tab-switcher>
+ <div :label="$t('settings.general')">
+ <div class="setting-item">
+ <h2>{{ $t('settings.interface') }}</h2>
+ <ul class="setting-list">
<li>
- <input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal">
- <label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label>
+ <interface-language-switcher />
+ </li>
+ <li v-if="instanceSpecificPanelPresent">
+ <input
+ id="hideISP"
+ v-model="hideISPLocal"
+ type="checkbox"
+ >
+ <label for="hideISP">{{ $t('settings.hide_isp') }}</label>
</li>
</ul>
- </li>
- <li>
- <input type="checkbox" id="autoload" v-model="autoLoadLocal">
- <label for="autoload">{{$t('settings.autoload')}}</label>
- </li>
- <li>
- <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
- <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
- </li>
- </ul>
- </div>
-
- <div class="setting-item">
- <h2>{{$t('settings.composing')}}</h2>
- <ul class="setting-list">
- <li>
- <input type="checkbox" id="scopeCopy" v-model="scopeCopyLocal">
- <label for="scopeCopy">
- {{$t('settings.scope_copy')}} {{$t('settings.instance_default', { value: scopeCopyDefault })}}
- </label>
- </li>
- <li>
- <input type="checkbox" id="subjectHide" v-model="alwaysShowSubjectInputLocal">
- <label for="subjectHide">
- {{$t('settings.subject_input_always_show')}} {{$t('settings.instance_default', { value: alwaysShowSubjectInputDefault })}}
- </label>
- </li>
- <li>
- <div>
- {{$t('settings.subject_line_behavior')}}
- <label for="subjectLineBehavior" class="select">
- <select id="subjectLineBehavior" v-model="subjectLineBehaviorLocal">
- <option value="email">
- {{$t('settings.subject_line_email')}}
- {{subjectLineBehaviorDefault == 'email' ? $t('settings.instance_default_simple') : ''}}
- </option>
- <option value="masto">
- {{$t('settings.subject_line_mastodon')}}
- {{subjectLineBehaviorDefault == 'mastodon' ? $t('settings.instance_default_simple') : ''}}
- </option>
- <option value="noop">
- {{$t('settings.subject_line_noop')}}
- {{subjectLineBehaviorDefault == 'noop' ? $t('settings.instance_default_simple') : ''}}
- </option>
- </select>
- <i class="icon-down-open"/>
- </label>
- </div>
- </li>
- <li>
- <div>
- {{$t('settings.post_status_content_type')}}
- <label for="postContentType" class="select">
- <select id="postContentType" v-model="postContentTypeLocal">
- <option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
- {{$t(`post_status.content_type["${postFormat}"]`)}}
- {{postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : ''}}
- </option>
- </select>
- <i class="icon-down-open"/>
- </label>
- </div>
- </li>
- <li>
- <input type="checkbox" id="minimalScopesMode" v-model="minimalScopesModeLocal">
- <label for="minimalScopesMode">
- {{$t('settings.minimal_scopes_mode')}} {{$t('settings.instance_default', { value: minimalScopesModeDefault })}}
- </label>
- </li>
- <li>
- <input type="checkbox" id="autohideFloatingPostButton" v-model="autohideFloatingPostButtonLocal">
- <label for="autohideFloatingPostButton">{{$t('settings.autohide_floating_post_button')}}</label>
- </li>
- </ul>
- </div>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('nav.timeline') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <input
+ id="hideMutedPosts"
+ v-model="hideMutedPostsLocal"
+ type="checkbox"
+ >
+ <label for="hideMutedPosts">{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsDefault }) }}</label>
+ </li>
+ <li>
+ <input
+ id="collapseMessageWithSubject"
+ v-model="collapseMessageWithSubjectLocal"
+ type="checkbox"
+ >
+ <label for="collapseMessageWithSubject">{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectDefault }) }}</label>
+ </li>
+ <li>
+ <input
+ id="streaming"
+ v-model="streamingLocal"
+ type="checkbox"
+ >
+ <label for="streaming">{{ $t('settings.streaming') }}</label>
+ <ul
+ class="setting-list suboptions"
+ :class="[{disabled: !streamingLocal}]"
+ >
+ <li>
+ <input
+ id="pauseOnUnfocused"
+ v-model="pauseOnUnfocusedLocal"
+ :disabled="!streamingLocal"
+ type="checkbox"
+ >
+ <label for="pauseOnUnfocused">{{ $t('settings.pause_on_unfocused') }}</label>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <input
+ id="autoload"
+ v-model="autoLoadLocal"
+ type="checkbox"
+ >
+ <label for="autoload">{{ $t('settings.autoload') }}</label>
+ </li>
+ <li>
+ <input
+ id="hoverPreview"
+ v-model="hoverPreviewLocal"
+ type="checkbox"
+ >
+ <label for="hoverPreview">{{ $t('settings.reply_link_preview') }}</label>
+ </li>
+ </ul>
+ </div>
- <div class="setting-item">
- <h2>{{$t('settings.attachments')}}</h2>
- <ul class="setting-list">
- <li>
- <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
- <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
- </li>
- <li>
- <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
- <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
- </li>
- <li>
- <label for="maxThumbnails">{{$t('settings.max_thumbnails')}}</label>
- <input class="number-input" type="number" id="maxThumbnails" v-model.number="maxThumbnails" min="0" step="1">
- </li>
- <li>
- <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
- <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
- </li>
- <ul class="setting-list suboptions" >
- <li>
- <input :disabled="!hideNsfwLocal" type="checkbox" id="preloadImage" v-model="preloadImage">
- <label for="preloadImage">{{$t('settings.preload_images')}}</label>
- </li>
- <li>
- <input :disabled="!hideNsfwLocal" type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
- <label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label>
- </li>
- </ul>
- <li>
- <input type="checkbox" id="stopGifs" v-model="stopGifs">
- <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
- </li>
- <li>
- <input type="checkbox" id="loopVideo" v-model="loopVideoLocal">
- <label for="loopVideo">{{$t('settings.loop_video')}}</label>
- <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
+ <div class="setting-item">
+ <h2>{{ $t('settings.composing') }}</h2>
+ <ul class="setting-list">
<li>
- <input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal">
- <label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label>
- <div v-if="!loopSilentAvailable" class="unavailable">
- <i class="icon-globe"/>! {{$t('settings.limited_availability')}}
+ <input
+ id="scopeCopy"
+ v-model="scopeCopyLocal"
+ type="checkbox"
+ >
+ <label for="scopeCopy">
+ {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyDefault }) }}
+ </label>
+ </li>
+ <li>
+ <input
+ id="subjectHide"
+ v-model="alwaysShowSubjectInputLocal"
+ type="checkbox"
+ >
+ <label for="subjectHide">
+ {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputDefault }) }}
+ </label>
+ </li>
+ <li>
+ <div>
+ {{ $t('settings.subject_line_behavior') }}
+ <label
+ for="subjectLineBehavior"
+ class="select"
+ >
+ <select
+ id="subjectLineBehavior"
+ v-model="subjectLineBehaviorLocal"
+ >
+ <option value="email">
+ {{ $t('settings.subject_line_email') }}
+ {{ subjectLineBehaviorDefault == 'email' ? $t('settings.instance_default_simple') : '' }}
+ </option>
+ <option value="masto">
+ {{ $t('settings.subject_line_mastodon') }}
+ {{ subjectLineBehaviorDefault == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
+ </option>
+ <option value="noop">
+ {{ $t('settings.subject_line_noop') }}
+ {{ subjectLineBehaviorDefault == 'noop' ? $t('settings.instance_default_simple') : '' }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
</div>
</li>
+ <li v-if="postFormats.length > 0">
+ <div>
+ {{ $t('settings.post_status_content_type') }}
+ <label
+ for="postContentType"
+ class="select"
+ >
+ <select
+ id="postContentType"
+ v-model="postContentTypeLocal"
+ >
+ <option
+ v-for="postFormat in postFormats"
+ :key="postFormat"
+ :value="postFormat"
+ >
+ {{ $t(`post_status.content_type["${postFormat}"]`) }}
+ {{ postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : '' }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
+ </li>
+ <li>
+ <input
+ id="minimalScopesMode"
+ v-model="minimalScopesModeLocal"
+ type="checkbox"
+ >
+ <label for="minimalScopesMode">
+ {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeDefault }) }}
+ </label>
+ </li>
+ <li>
+ <input
+ id="autohideFloatingPostButton"
+ v-model="autohideFloatingPostButtonLocal"
+ type="checkbox"
+ >
+ <label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label>
+ </li>
</ul>
- </li>
- <li>
- <input type="checkbox" id="playVideosInModal" v-model="playVideosInModal">
- <label for="playVideosInModal">{{$t('settings.play_videos_in_modal')}}</label>
- </li>
- <li>
- <input type="checkbox" id="useContainFit" v-model="useContainFit">
- <label for="useContainFit">{{$t('settings.use_contain_fit')}}</label>
- </li>
- </ul>
- </div>
+ </div>
- <div class="setting-item">
- <h2>{{$t('settings.notifications')}}</h2>
- <ul class="setting-list">
- <li>
- <input type="checkbox" id="webPushNotifications" v-model="webPushNotificationsLocal">
- <label for="webPushNotifications">
- {{$t('settings.enable_web_push_notifications')}}
- </label>
- </li>
- </ul>
- </div>
- </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.attachments') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <input
+ id="hideAttachments"
+ v-model="hideAttachmentsLocal"
+ type="checkbox"
+ >
+ <label for="hideAttachments">{{ $t('settings.hide_attachments_in_tl') }}</label>
+ </li>
+ <li>
+ <input
+ id="hideAttachmentsInConv"
+ v-model="hideAttachmentsInConvLocal"
+ type="checkbox"
+ >
+ <label for="hideAttachmentsInConv">{{ $t('settings.hide_attachments_in_convo') }}</label>
+ </li>
+ <li>
+ <label for="maxThumbnails">{{ $t('settings.max_thumbnails') }}</label>
+ <input
+ id="maxThumbnails"
+ v-model.number="maxThumbnails"
+ class="number-input"
+ type="number"
+ min="0"
+ step="1"
+ >
+ </li>
+ <li>
+ <input
+ id="hideNsfw"
+ v-model="hideNsfwLocal"
+ type="checkbox"
+ >
+ <label for="hideNsfw">{{ $t('settings.nsfw_clickthrough') }}</label>
+ </li>
+ <ul class="setting-list suboptions">
+ <li>
+ <input
+ id="preloadImage"
+ v-model="preloadImage"
+ :disabled="!hideNsfwLocal"
+ type="checkbox"
+ >
+ <label for="preloadImage">{{ $t('settings.preload_images') }}</label>
+ </li>
+ <li>
+ <input
+ id="useOneClickNsfw"
+ v-model="useOneClickNsfw"
+ :disabled="!hideNsfwLocal"
+ type="checkbox"
+ >
+ <label for="useOneClickNsfw">{{ $t('settings.use_one_click_nsfw') }}</label>
+ </li>
+ </ul>
+ <li>
+ <input
+ id="stopGifs"
+ v-model="stopGifs"
+ type="checkbox"
+ >
+ <label for="stopGifs">{{ $t('settings.stop_gifs') }}</label>
+ </li>
+ <li>
+ <input
+ id="loopVideo"
+ v-model="loopVideoLocal"
+ type="checkbox"
+ >
+ <label for="loopVideo">{{ $t('settings.loop_video') }}</label>
+ <ul
+ class="setting-list suboptions"
+ :class="[{disabled: !streamingLocal}]"
+ >
+ <li>
+ <input
+ id="loopVideoSilentOnly"
+ v-model="loopVideoSilentOnlyLocal"
+ :disabled="!loopVideoLocal || !loopSilentAvailable"
+ type="checkbox"
+ >
+ <label for="loopVideoSilentOnly">{{ $t('settings.loop_video_silent_only') }}</label>
+ <div
+ v-if="!loopSilentAvailable"
+ class="unavailable"
+ >
+ <i class="icon-globe" />! {{ $t('settings.limited_availability') }}
+ </div>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <input
+ id="playVideosInModal"
+ v-model="playVideosInModal"
+ type="checkbox"
+ >
+ <label for="playVideosInModal">{{ $t('settings.play_videos_in_modal') }}</label>
+ </li>
+ <li>
+ <input
+ id="useContainFit"
+ v-model="useContainFit"
+ type="checkbox"
+ >
+ <label for="useContainFit">{{ $t('settings.use_contain_fit') }}</label>
+ </li>
+ </ul>
+ </div>
- <div :label="$t('settings.theme')" >
- <div class="setting-item">
- <style-switcher></style-switcher>
- </div>
- </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.notifications') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <input
+ id="webPushNotifications"
+ v-model="webPushNotificationsLocal"
+ type="checkbox"
+ >
+ <label for="webPushNotifications">
+ {{ $t('settings.enable_web_push_notifications') }}
+ </label>
+ </li>
+ </ul>
+ </div>
+ </div>
- <div :label="$t('settings.filtering')" >
- <div class="setting-item">
- <div class="select-multiple">
- <span class="label">{{$t('settings.notification_visibility')}}</span>
- <ul class="option-list">
- <li>
- <input type="checkbox" id="notification-visibility-likes" v-model="notificationVisibilityLocal.likes">
- <label for="notification-visibility-likes">
- {{$t('settings.notification_visibility_likes')}}
+ <div :label="$t('settings.theme')">
+ <div class="setting-item">
+ <style-switcher />
+ </div>
+ </div>
+
+ <div :label="$t('settings.filtering')">
+ <div class="setting-item">
+ <div class="select-multiple">
+ <span class="label">{{ $t('settings.notification_visibility') }}</span>
+ <ul class="option-list">
+ <li>
+ <input
+ id="notification-visibility-likes"
+ v-model="notificationVisibilityLocal.likes"
+ type="checkbox"
+ >
+ <label for="notification-visibility-likes">
+ {{ $t('settings.notification_visibility_likes') }}
+ </label>
+ </li>
+ <li>
+ <input
+ id="notification-visibility-repeats"
+ v-model="notificationVisibilityLocal.repeats"
+ type="checkbox"
+ >
+ <label for="notification-visibility-repeats">
+ {{ $t('settings.notification_visibility_repeats') }}
+ </label>
+ </li>
+ <li>
+ <input
+ id="notification-visibility-follows"
+ v-model="notificationVisibilityLocal.follows"
+ type="checkbox"
+ >
+ <label for="notification-visibility-follows">
+ {{ $t('settings.notification_visibility_follows') }}
+ </label>
+ </li>
+ <li>
+ <input
+ id="notification-visibility-mentions"
+ v-model="notificationVisibilityLocal.mentions"
+ type="checkbox"
+ >
+ <label for="notification-visibility-mentions">
+ {{ $t('settings.notification_visibility_mentions') }}
+ </label>
+ </li>
+ </ul>
+ </div>
+ <div>
+ {{ $t('settings.replies_in_timeline') }}
+ <label
+ for="replyVisibility"
+ class="select"
+ >
+ <select
+ id="replyVisibility"
+ v-model="replyVisibilityLocal"
+ >
+ <option
+ value="all"
+ selected
+ >{{ $t('settings.reply_visibility_all') }}</option>
+ <option value="following">{{ $t('settings.reply_visibility_following') }}</option>
+ <option value="self">{{ $t('settings.reply_visibility_self') }}</option>
+ </select>
+ <i class="icon-down-open" />
</label>
- </li>
- <li>
- <input type="checkbox" id="notification-visibility-repeats" v-model="notificationVisibilityLocal.repeats">
- <label for="notification-visibility-repeats">
- {{$t('settings.notification_visibility_repeats')}}
+ </div>
+ <div>
+ <input
+ id="hidePostStats"
+ v-model="hidePostStatsLocal"
+ type="checkbox"
+ >
+ <label for="hidePostStats">
+ {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsDefault }) }}
</label>
- </li>
- <li>
- <input type="checkbox" id="notification-visibility-follows" v-model="notificationVisibilityLocal.follows">
- <label for="notification-visibility-follows">
- {{$t('settings.notification_visibility_follows')}}
+ </div>
+ <div>
+ <input
+ id="hideUserStats"
+ v-model="hideUserStatsLocal"
+ type="checkbox"
+ >
+ <label for="hideUserStats">
+ {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsDefault }) }}
</label>
- </li>
- <li>
- <input type="checkbox" id="notification-visibility-mentions" v-model="notificationVisibilityLocal.mentions">
- <label for="notification-visibility-mentions">
- {{$t('settings.notification_visibility_mentions')}}
+ </div>
+ </div>
+ <div class="setting-item">
+ <div>
+ <p>{{ $t('settings.filtering_explanation') }}</p>
+ <textarea
+ id="muteWords"
+ v-model="muteWordsString"
+ />
+ </div>
+ <div>
+ <input
+ id="hideFilteredStatuses"
+ v-model="hideFilteredStatusesLocal"
+ type="checkbox"
+ >
+ <label for="hideFilteredStatuses">
+ {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesDefault }) }}
</label>
- </li>
- </ul>
- </div>
- <div>
- {{$t('settings.replies_in_timeline')}}
- <label for="replyVisibility" class="select">
- <select id="replyVisibility" v-model="replyVisibilityLocal">
- <option value="all" selected>{{$t('settings.reply_visibility_all')}}</option>
- <option value="following">{{$t('settings.reply_visibility_following')}}</option>
- <option value="self">{{$t('settings.reply_visibility_self')}}</option>
- </select>
- <i class="icon-down-open"/>
- </label>
- </div>
- <div>
- <input type="checkbox" id="hidePostStats" v-model="hidePostStatsLocal">
- <label for="hidePostStats">
- {{$t('settings.hide_post_stats')}} {{$t('settings.instance_default', { value: hidePostStatsDefault })}}
- </label>
- </div>
- <div>
- <input type="checkbox" id="hideUserStats" v-model="hideUserStatsLocal">
- <label for="hideUserStats">
- {{$t('settings.hide_user_stats')}} {{$t('settings.instance_default', { value: hideUserStatsDefault })}}
- </label>
- </div>
- </div>
- <div class="setting-item">
- <div>
- <p>{{$t('settings.filtering_explanation')}}</p>
- <textarea id="muteWords" v-model="muteWordsString"></textarea>
- </div>
- <div>
- <input type="checkbox" id="hideFilteredStatuses" v-model="hideFilteredStatusesLocal">
- <label for="hideFilteredStatuses">
- {{$t('settings.hide_filtered_statuses')}} {{$t('settings.instance_default', { value: hideFilteredStatusesDefault })}}
- </label>
+ </div>
+ </div>
</div>
- </div>
- </div>
- <div :label="$t('settings.version.title')" >
- <div class="setting-item">
- <ul class="setting-list">
- <li>
- <p>{{$t('settings.version.backend_version')}}</p>
- <ul class="option-list">
+ <div :label="$t('settings.version.title')">
+ <div class="setting-item">
+ <ul class="setting-list">
<li>
- <a :href="backendVersionLink" target="_blank">{{backendVersion}}</a>
+ <p>{{ $t('settings.version.backend_version') }}</p>
+ <ul class="option-list">
+ <li>
+ <a
+ :href="backendVersionLink"
+ target="_blank"
+ >{{ backendVersion }}</a>
+ </li>
+ </ul>
</li>
- </ul>
- </li>
- <li>
- <p>{{$t('settings.version.frontend_version')}}</p>
- <ul class="option-list">
<li>
- <a :href="frontendVersionLink" target="_blank">{{frontendVersion}}</a>
+ <p>{{ $t('settings.version.frontend_version') }}</p>
+ <ul class="option-list">
+ <li>
+ <a
+ :href="frontendVersionLink"
+ target="_blank"
+ >{{ frontendVersion }}</a>
+ </li>
+ </ul>
</li>
</ul>
- </li>
- </ul>
- </div>
- </div>
- </tab-switcher>
-</keep-alive>
+ </div>
+ </div>
+ </tab-switcher>
+ </keep-alive>
+ </div>
</div>
-</div>
</template>
<script src="./settings.js">
diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue
index 744925d4..de8a42d1 100644
--- a/src/components/shadow_control/shadow_control.vue
+++ b/src/components/shadow_control/shadow_control.vue
@@ -1,134 +1,207 @@
<template>
-<div class="shadow-control" :class="{ disabled: !present }">
- <div class="shadow-preview-container">
- <div :disabled="!present" class="y-shift-control">
- <input
- v-model="selected.y"
+ <div
+ class="shadow-control"
+ :class="{ disabled: !present }"
+ >
+ <div class="shadow-preview-container">
+ <div
:disabled="!present"
- class="input-number"
- type="number">
- <div class="wrap">
+ class="y-shift-control"
+ >
<input
v-model="selected.y"
:disabled="!present"
- class="input-range"
- type="range"
- max="20"
- min="-20">
+ class="input-number"
+ type="number"
+ >
+ <div class="wrap">
+ <input
+ v-model="selected.y"
+ :disabled="!present"
+ class="input-range"
+ type="range"
+ max="20"
+ min="-20"
+ >
+ </div>
</div>
- </div>
- <div class="preview-window">
- <div class="preview-block" :style="style"></div>
- </div>
- <div :disabled="!present" class="x-shift-control">
- <input
- v-model="selected.x"
+ <div class="preview-window">
+ <div
+ class="preview-block"
+ :style="style"
+ />
+ </div>
+ <div
:disabled="!present"
- class="input-number"
- type="number">
- <div class="wrap">
+ class="x-shift-control"
+ >
<input
v-model="selected.x"
:disabled="!present"
- class="input-range"
- type="range"
- max="20"
- min="-20">
+ class="input-number"
+ type="number"
+ >
+ <div class="wrap">
+ <input
+ v-model="selected.x"
+ :disabled="!present"
+ class="input-range"
+ type="range"
+ max="20"
+ min="-20"
+ >
+ </div>
</div>
</div>
- </div>
- <div class="shadow-tweak">
- <div :disabled="usingFallback" class="id-control style-control">
- <label for="shadow-switcher" class="select" :disabled="!ready || usingFallback">
- <select
- v-model="selectedId" class="shadow-switcher"
+ <div class="shadow-tweak">
+ <div
+ :disabled="usingFallback"
+ class="id-control style-control"
+ >
+ <label
+ for="shadow-switcher"
+ class="select"
:disabled="!ready || usingFallback"
- id="shadow-switcher">
- <option v-for="(shadow, index) in cValue" :value="index">
- {{$t('settings.style.shadows.shadow_id', { value: index })}}
- </option>
- </select>
- <i class="icon-down-open"/>
- </label>
- <button class="btn btn-default" :disabled="!ready || !present" @click="del">
- <i class="icon-cancel"/>
- </button>
- <button class="btn btn-default" :disabled="!moveUpValid" @click="moveUp">
- <i class="icon-up-open"/>
- </button>
- <button class="btn btn-default" :disabled="!moveDnValid" @click="moveDn">
- <i class="icon-down-open"/>
- </button>
- <button class="btn btn-default" :disabled="usingFallback" @click="add">
- <i class="icon-plus"/>
- </button>
- </div>
- <div :disabled="!present" class="inset-control style-control">
- <label for="inset" class="label">
- {{$t('settings.style.shadows.inset')}}
- </label>
- <input
- v-model="selected.inset"
+ >
+ <select
+ id="shadow-switcher"
+ v-model="selectedId"
+ class="shadow-switcher"
+ :disabled="!ready || usingFallback"
+ >
+ <option
+ v-for="(shadow, index) in cValue"
+ :key="index"
+ :value="index"
+ >
+ {{ $t('settings.style.shadows.shadow_id', { value: index }) }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ <button
+ class="btn btn-default"
+ :disabled="!ready || !present"
+ @click="del"
+ >
+ <i class="icon-cancel" />
+ </button>
+ <button
+ class="btn btn-default"
+ :disabled="!moveUpValid"
+ @click="moveUp"
+ >
+ <i class="icon-up-open" />
+ </button>
+ <button
+ class="btn btn-default"
+ :disabled="!moveDnValid"
+ @click="moveDn"
+ >
+ <i class="icon-down-open" />
+ </button>
+ <button
+ class="btn btn-default"
+ :disabled="usingFallback"
+ @click="add"
+ >
+ <i class="icon-plus" />
+ </button>
+ </div>
+ <div
:disabled="!present"
- name="inset"
- id="inset"
- class="input-inset"
- type="checkbox">
- <label class="checkbox-label" for="inset"></label>
- </div>
- <div :disabled="!present" class="blur-control style-control">
- <label for="spread" class="label">
- {{$t('settings.style.shadows.blur')}}
- </label>
- <input
- v-model="selected.blur"
+ class="inset-control style-control"
+ >
+ <label
+ for="inset"
+ class="label"
+ >
+ {{ $t('settings.style.shadows.inset') }}
+ </label>
+ <input
+ id="inset"
+ v-model="selected.inset"
+ :disabled="!present"
+ name="inset"
+ class="input-inset"
+ type="checkbox"
+ >
+ <label
+ class="checkbox-label"
+ for="inset"
+ />
+ </div>
+ <div
:disabled="!present"
- name="blur"
- id="blur"
- class="input-range"
- type="range"
- max="20"
- min="0">
- <input
- v-model="selected.blur"
+ class="blur-control style-control"
+ >
+ <label
+ for="spread"
+ class="label"
+ >
+ {{ $t('settings.style.shadows.blur') }}
+ </label>
+ <input
+ id="blur"
+ v-model="selected.blur"
+ :disabled="!present"
+ name="blur"
+ class="input-range"
+ type="range"
+ max="20"
+ min="0"
+ >
+ <input
+ v-model="selected.blur"
+ :disabled="!present"
+ class="input-number"
+ type="number"
+ min="0"
+ >
+ </div>
+ <div
:disabled="!present"
- class="input-number"
- type="number"
- min="0">
- </div>
- <div :disabled="!present" class="spread-control style-control">
- <label for="spread" class="label">
- {{$t('settings.style.shadows.spread')}}
- </label>
- <input
- v-model="selected.spread"
+ class="spread-control style-control"
+ >
+ <label
+ for="spread"
+ class="label"
+ >
+ {{ $t('settings.style.shadows.spread') }}
+ </label>
+ <input
+ id="spread"
+ v-model="selected.spread"
+ :disabled="!present"
+ name="spread"
+ class="input-range"
+ type="range"
+ max="20"
+ min="-20"
+ >
+ <input
+ v-model="selected.spread"
+ :disabled="!present"
+ class="input-number"
+ type="number"
+ >
+ </div>
+ <ColorInput
+ v-model="selected.color"
:disabled="!present"
- name="spread"
- id="spread"
- class="input-range"
- type="range"
- max="20"
- min="-20">
- <input
- v-model="selected.spread"
+ :label="$t('settings.style.common.color')"
+ name="shadow"
+ />
+ <OpacityInput
+ v-model="selected.alpha"
:disabled="!present"
- class="input-number"
- type="number">
+ />
+ <p>
+ {{ $t('settings.style.shadows.hint') }}
+ </p>
</div>
- <ColorInput
- v-model="selected.color"
- :disabled="!present"
- :label="$t('settings.style.common.color')"
- name="shadow"/>
- <OpacityInput
- v-model="selected.alpha"
- :disabled="!present"/>
- <p>
- {{$t('settings.style.shadows.hint')}}
- </p>
</div>
-</div>
</template>
<script src="./shadow_control.js" ></script>
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 6428b1b0..5b2d4473 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -1,63 +1,98 @@
<template>
- <div class="side-drawer-container"
+ <div
+ class="side-drawer-container"
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
>
- <div class="side-drawer-darken" :class="{ 'side-drawer-darken-closed': closed}" />
- <div class="side-drawer"
+ <div
+ class="side-drawer-darken"
+ :class="{ 'side-drawer-darken-closed': closed}"
+ />
+ <div
+ class="side-drawer"
:class="{'side-drawer-closed': closed}"
@touchstart="touchStart"
@touchmove="touchMove"
>
- <div class="side-drawer-heading" @click="toggleDrawer">
- <UserCard :user="currentUser" :hideBio="true" v-if="currentUser"/>
- <div class="side-drawer-logo-wrapper" v-else>
- <img :src="logo"/>
- <span>{{sitename}}</span>
+ <div
+ class="side-drawer-heading"
+ @click="toggleDrawer"
+ >
+ <UserCard
+ v-if="currentUser"
+ :user="currentUser"
+ :hide-bio="true"
+ />
+ <div
+ v-else
+ class="side-drawer-logo-wrapper"
+ >
+ <img :src="logo">
+ <span>{{ sitename }}</span>
</div>
</div>
<ul>
- <li v-if="!currentUser" @click="toggleDrawer">
+ <li
+ v-if="!currentUser"
+ @click="toggleDrawer"
+ >
<router-link :to="{ name: 'login' }">
{{ $t("login.login") }}
</router-link>
</li>
- <li v-if="currentUser" @click="toggleDrawer">
+ <li
+ v-if="currentUser"
+ @click="toggleDrawer"
+ >
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
</router-link>
</li>
- <li v-if="currentUser" @click="toggleDrawer">
+ <li
+ v-if="currentUser"
+ @click="toggleDrawer"
+ >
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
{{ $t("nav.interactions") }}
</router-link>
</li>
</ul>
<ul>
- <li v-if="currentUser" @click="toggleDrawer">
+ <li
+ v-if="currentUser"
+ @click="toggleDrawer"
+ >
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
</router-link>
</li>
- <li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
- <router-link to='/friend-requests'>
+ <li
+ v-if="currentUser && currentUser.locked"
+ @click="toggleDrawer"
+ >
+ <router-link to="/friend-requests">
{{ $t("nav.friend_requests") }}
- <span v-if='followRequestCount > 0' class="badge follow-request-count">
- {{followRequestCount}}
+ <span
+ v-if="followRequestCount > 0"
+ class="badge follow-request-count"
+ >
+ {{ followRequestCount }}
</span>
-
</router-link>
</li>
<li @click="toggleDrawer">
- <router-link to='/main/public'>
+ <router-link to="/main/public">
{{ $t("nav.public_tl") }}
</router-link>
</li>
<li @click="toggleDrawer">
- <router-link to='/main/all'>
+ <router-link to="/main/all">
{{ $t("nav.twkn") }}
</router-link>
</li>
- <li v-if="currentUser && chat" @click="toggleDrawer">
+ <li
+ v-if="currentUser && chat"
+ @click="toggleDrawer"
+ >
<router-link :to="{ name: 'chat' }">
{{ $t("nav.chat") }}
</router-link>
@@ -65,11 +100,14 @@
</ul>
<ul>
<li @click="toggleDrawer">
- <router-link :to="{ name: 'user-search' }">
- {{ $t("nav.user_search") }}
+ <router-link :to="{ name: 'search' }">
+ {{ $t("nav.search") }}
</router-link>
</li>
- <li v-if="currentUser && suggestionsEnabled" @click="toggleDrawer">
+ <li
+ v-if="currentUser && suggestionsEnabled"
+ @click="toggleDrawer"
+ >
<router-link :to="{ name: 'who-to-follow' }">
{{ $t("nav.who_to_follow") }}
</router-link>
@@ -84,17 +122,24 @@
{{ $t("nav.about") }}
</router-link>
</li>
- <li v-if="currentUser" @click="toggleDrawer">
- <a @click="doLogout" href="#">
+ <li
+ v-if="currentUser"
+ @click="toggleDrawer"
+ >
+ <a
+ href="#"
+ @click="doLogout"
+ >
{{ $t("login.logout") }}
</a>
</li>
</ul>
</div>
- <div class="side-drawer-click-outside"
- @click.stop.prevent="toggleDrawer"
+ <div
+ class="side-drawer-click-outside"
:class="{'side-drawer-click-outside-closed': closed}"
- ></div>
+ @click.stop.prevent="toggleDrawer"
+ />
</div>
</template>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index ea4c2b9d..502d9583 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,6 +1,7 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
+import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.vue'
@@ -8,6 +9,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
+import Timeago from '../timeago/timeago.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
@@ -108,8 +110,9 @@ const Status = {
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
+ const statusSummary = this.status.summary.toLowerCase()
const hits = filter(this.muteWords, (muteWord) => {
- return statusText.includes(muteWord.toLowerCase())
+ return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
})
return hits
@@ -171,12 +174,13 @@ const Status = {
if (this.status.type === 'retweet') {
return false
}
- var checkFollowing = this.$store.state.config.replyVisibility === 'following'
+ const checkFollowing = this.$store.state.config.replyVisibility === 'following'
for (var i = 0; i < this.status.attentions.length; ++i) {
if (this.status.user.id === this.status.attentions[i].id) {
continue
}
- if (checkFollowing && this.$store.getters.findUser(this.status.attentions[i].id).following) {
+ const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)
+ if (checkFollowing && taggedUser && taggedUser.following) {
return false
}
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
@@ -216,10 +220,10 @@ const Status = {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
- ? this.$store.state.instance.subjectLineBehavior
- : this.$store.state.config.subjectLineBehavior
+ ? this.$store.state.instance.subjectLineBehavior
+ : this.$store.state.config.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i)
- if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
+ if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
return decodedSummary
} else if (behavior === 'email') {
return 're: '.concat(decodedSummary)
@@ -277,6 +281,11 @@ const Status = {
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
+ },
+ hidePostStats () {
+ return typeof this.$store.state.config.hidePostStats === 'undefined'
+ ? this.$store.state.instance.hidePostStats
+ : this.$store.state.config.hidePostStats
}
},
components: {
@@ -285,11 +294,13 @@ const Status = {
RetweetButton,
ExtraButtons,
PostStatusForm,
+ Poll,
UserCard,
UserAvatar,
Gallery,
LinkPreview,
- AvatarList
+ AvatarList,
+ Timeago
},
methods: {
visibilityIcon (visibility) {
@@ -311,11 +322,8 @@ const Status = {
this.error = undefined
},
linkClicked (event) {
- let { target } = event
- if (target.tagName === 'SPAN') {
- target = target.parentNode
- }
- if (target.tagName === 'A') {
+ const target = event.target.closest('.status-content a')
+ if (target) {
if (target.className.match(/mention/)) {
const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
@@ -327,7 +335,7 @@ const Status = {
return
}
}
- if (target.className.match(/hashtag/)) {
+ if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
// Extract tag name from link url
const tag = extractTagFromUrl(target.href)
if (tag) {
@@ -377,7 +385,7 @@ const Status = {
this.preview = find(statuses, { 'id': targetId })
// or if we have to fetch it
if (!this.preview) {
- this.$store.state.api.backendInteractor.fetchStatus({id}).then((status) => {
+ this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => {
this.preview = status
})
}
@@ -414,6 +422,18 @@ const Status = {
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
}
}
+ },
+ 'status.repeat_num': function (num) {
+ // refetch repeats when repeat_num is changed in any way
+ if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {
+ this.$store.dispatch('fetchRepeats', this.status.id)
+ }
+ },
+ 'status.fave_num': function (num) {
+ // refetch favs when fave_num is changed in any way
+ if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
+ this.$store.dispatch('fetchFavs', this.status.id)
+ }
}
},
filters: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index e1dd81ac..64218f6e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,186 +1,431 @@
<template>
- <div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
- <div v-if="error" class="alert error">
- {{error}}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <!-- eslint-disable vue/no-v-html -->
+ <div
+ v-if="!hideStatus"
+ class="status-el"
+ :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"
+ >
+ <div
+ v-if="error"
+ class="alert error"
+ >
+ {{ error }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearError"
+ />
</div>
<template v-if="muted && !isPreview">
<div class="media status container muted">
<small>
<router-link :to="userProfileLink">
- {{status.user.screen_name}}
+ {{ status.user.screen_name }}
</router-link>
</small>
- <small class="muteWords">{{muteWordHits.join(', ')}}</small>
- <a href="#" class="unmute" @click.prevent="toggleMute"><i class="button-icon icon-eye-off"></i></a>
+ <small class="muteWords">{{ muteWordHits.join(', ') }}</small>
+ <a
+ href="#"
+ class="unmute"
+ @click.prevent="toggleMute"
+ ><i class="button-icon icon-eye-off" /></a>
</div>
</template>
<template v-else>
- <div v-if="showPinned && statusoid.pinned" class="status-pin">
- <i class="fa icon-pin faint"></i>
- <span class="faint">{{$t('status.pinned')}}</span>
+ <div
+ v-if="showPinned"
+ class="status-pin"
+ >
+ <i class="fa icon-pin faint" />
+ <span class="faint">{{ $t('status.pinned') }}</span>
</div>
- <div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
- <UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :user="statusoid.user"/>
+ <div
+ v-if="retweet && !noHeading && !inConversation"
+ :class="[repeaterClass, { highlighted: repeaterStyle }]"
+ :style="[repeaterStyle]"
+ class="media container retweet-info"
+ >
+ <UserAvatar
+ v-if="retweet"
+ class="media-left"
+ :better-shadow="betterShadow"
+ :user="statusoid.user"
+ />
<div class="media-body faint">
<span class="user-name">
- <router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
- <router-link v-else :to="retweeterProfileLink">{{retweeter}}</router-link>
+ <router-link
+ v-if="retweeterHtml"
+ :to="retweeterProfileLink"
+ v-html="retweeterHtml"
+ />
+ <router-link
+ v-else
+ :to="retweeterProfileLink"
+ >{{ retweeter }}</router-link>
</span>
- <i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
- {{$t('timeline.repeated')}}
+ <i
+ class="fa icon-retweet retweeted"
+ :title="$t('tool_tip.repeat')"
+ />
+ {{ $t('timeline.repeated') }}
</div>
</div>
- <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status" :data-tags="tags">
- <div v-if="!noHeading" class="media-left">
- <router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
- <UserAvatar :compact="compact" :betterShadow="betterShadow" :user="status.user"/>
+ <div
+ :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]"
+ :style="[ userStyle ]"
+ class="media status"
+ :data-tags="tags"
+ >
+ <div
+ v-if="!noHeading"
+ class="media-left"
+ >
+ <router-link
+ :to="userProfileLink"
+ @click.stop.prevent.capture.native="toggleUserExpanded"
+ >
+ <UserAvatar
+ :compact="compact"
+ :better-shadow="betterShadow"
+ :user="status.user"
+ />
</router-link>
</div>
<div class="status-body">
- <UserCard :user="status.user" :rounded="true" :bordered="true" class="status-usercard" v-if="userExpanded"/>
- <div v-if="!noHeading" class="media-heading">
+ <UserCard
+ v-if="userExpanded"
+ :user="status.user"
+ :rounded="true"
+ :bordered="true"
+ class="status-usercard"
+ />
+ <div
+ v-if="!noHeading"
+ class="media-heading"
+ >
<div class="heading-name-row">
<div class="name-and-account-name">
- <h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
- <h4 class="user-name" v-else>{{status.user.name}}</h4>
- <router-link class="account-name" :to="userProfileLink">
- {{status.user.screen_name}}
+ <h4
+ v-if="status.user.name_html"
+ class="user-name"
+ v-html="status.user.name_html"
+ />
+ <h4
+ v-else
+ class="user-name"
+ >
+ {{ status.user.name }}
+ </h4>
+ <router-link
+ class="account-name"
+ :to="userProfileLink"
+ >
+ {{ status.user.screen_name }}
</router-link>
</div>
<span class="heading-right">
- <router-link class="timeago faint-link" :to="{ name: 'conversation', params: { id: status.id } }">
- <timeago :since="status.created_at" :auto-update="60"></timeago>
+ <router-link
+ class="timeago faint-link"
+ :to="{ name: 'conversation', params: { id: status.id } }"
+ >
+ <Timeago
+ :time="status.created_at"
+ :auto-update="60"
+ />
</router-link>
- <div class="button-icon visibility-icon" v-if="status.visibility">
- <i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
+ <div
+ v-if="status.visibility"
+ class="button-icon visibility-icon"
+ >
+ <i
+ :class="visibilityIcon(status.visibility)"
+ :title="status.visibility | capitalize"
+ />
</div>
- <a :href="status.external_url" target="_blank" v-if="!status.is_local && !isPreview" class="source_url" title="Source">
- <i class="button-icon icon-link-ext-alt"></i>
+ <a
+ v-if="!status.is_local && !isPreview"
+ :href="status.external_url"
+ target="_blank"
+ class="source_url"
+ title="Source"
+ >
+ <i class="button-icon icon-link-ext-alt" />
</a>
<template v-if="expandable && !isPreview">
- <a href="#" @click.prevent="toggleExpanded" title="Expand">
- <i class="button-icon icon-plus-squared"></i>
+ <a
+ href="#"
+ title="Expand"
+ @click.prevent="toggleExpanded"
+ >
+ <i class="button-icon icon-plus-squared" />
</a>
</template>
- <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
+ <a
+ v-if="unmuted"
+ href="#"
+ @click.prevent="toggleMute"
+ ><i class="button-icon icon-eye-off" /></a>
</span>
</div>
<div class="heading-reply-row">
- <div v-if="isReply" class="reply-to-and-accountname">
- <a class="reply-to"
- href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
+ <div
+ v-if="isReply"
+ class="reply-to-and-accountname"
+ >
+ <a
+ class="reply-to"
+ href="#"
:aria-label="$t('tool_tip.reply')"
+ @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
@mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
@mouseleave.prevent.stop="replyLeave()"
>
- <i class="button-icon icon-reply" v-if="!isPreview"></i>
- <span class="faint-link reply-to-text">{{$t('status.reply_to')}}</span>
+ <i
+ v-if="!isPreview"
+ class="button-icon icon-reply"
+ />
+ <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
</a>
<router-link :to="replyProfileLink">
- {{replyToName}}
+ {{ replyToName }}
</router-link>
- <span class="faint replies-separator" v-if="replies && replies.length">
+ <span
+ v-if="replies && replies.length"
+ class="faint replies-separator"
+ >
-
</span>
</div>
- <div class="replies" v-if="inConversation && !isPreview">
- <span class="faint" v-if="replies && replies.length">{{$t('status.replies_list')}}</span>
- <span class="reply-link faint" v-if="replies" v-for="reply in replies">
- <a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}</a>
- </span>
+ <div
+ v-if="inConversation && !isPreview"
+ class="replies"
+ >
+ <span
+ v-if="replies && replies.length"
+ class="faint"
+ >{{ $t('status.replies_list') }}</span>
+ <template v-if="replies">
+ <span
+ v-for="reply in replies"
+ :key="reply.id"
+ class="reply-link faint"
+ >
+ <a
+ href="#"
+ @click.prevent="gotoOriginal(reply.id)"
+ @mouseenter="replyEnter(reply.id, $event)"
+ @mouseout="replyLeave()"
+ >{{ reply.name }}</a>
+ </span>
+ </template>
</div>
</div>
-
-
</div>
- <div v-if="showPreview" class="status-preview-container">
- <status class="status-preview"
+ <div
+ v-if="showPreview"
+ class="status-preview-container"
+ >
+ <status
v-if="preview"
- :isPreview="true"
+ class="status-preview"
+ :is-preview="true"
:statusoid="preview"
:compact="true"
/>
- <div v-else class="status-preview status-preview-loading">
- <i class="icon-spin4 animate-spin"></i>
+ <div
+ v-else
+ class="status-preview status-preview-loading"
+ >
+ <i class="icon-spin4 animate-spin" />
</div>
</div>
- <div class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" v-if="longSubject">
- <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="!showingLongSubject" href="#" @click.prevent="showingLongSubject=true">{{$t("general.show_more")}}</a>
- <div @click.prevent="linkClicked" class="status-content media-body" v-html="contentHtml"></div>
- <a v-if="showingLongSubject" href="#" class="status-unhider" @click.prevent="showingLongSubject=false">{{$t("general.show_less")}}</a>
+ <div
+ v-if="longSubject"
+ class="status-content-wrapper"
+ :class="{ 'tall-status': !showingLongSubject }"
+ >
+ <a
+ v-if="!showingLongSubject"
+ class="tall-status-hider"
+ :class="{ 'tall-status-hider_focused': isFocused }"
+ href="#"
+ @click.prevent="showingLongSubject=true"
+ >{{ $t("general.show_more") }}</a>
+ <div
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="contentHtml"
+ />
+ <a
+ v-if="showingLongSubject"
+ href="#"
+ class="status-unhider"
+ @click.prevent="showingLongSubject=false"
+ >{{ $t("general.show_less") }}</a>
+ </div>
+ <div
+ v-else
+ :class="{'tall-status': hideTallStatus}"
+ class="status-content-wrapper"
+ >
+ <a
+ v-if="hideTallStatus"
+ class="tall-status-hider"
+ :class="{ 'tall-status-hider_focused': isFocused }"
+ href="#"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_more") }}</a>
+ <div
+ v-if="!hideSubjectStatus"
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="contentHtml"
+ />
+ <div
+ v-else
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="status.summary_html"
+ />
+ <a
+ v-if="hideSubjectStatus"
+ href="#"
+ class="cw-status-hider"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_more") }}</a>
+ <a
+ v-if="showingMore"
+ href="#"
+ class="status-unhider"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_less") }}</a>
</div>
- <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" v-else>
- <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">{{$t("general.show_more")}}</a>
- <div @click.prevent="linkClicked" class="status-content media-body" v-html="contentHtml" v-if="!hideSubjectStatus"></div>
- <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
- <a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">{{$t("general.show_more")}}</a>
- <a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a>
+
+ <div v-if="status.poll && status.poll.options">
+ <poll :base-poll="status.poll" />
</div>
- <div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
+ <div
+ v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
+ class="attachments media-body"
+ >
<attachment
- class="non-gallery"
v-for="attachment in nonGalleryAttachments"
+ :key="attachment.id"
+ class="non-gallery"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
:attachment="attachment"
- :allowPlay="true"
- :setMedia="setMedia()"
- :key="attachment.id"
+ :allow-play="true"
+ :set-media="setMedia()"
/>
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
- :setMedia="setMedia()"
+ :set-media="setMedia()"
/>
</div>
- <div v-if="status.card && !hideSubjectStatus && !noHeading" class="link-preview media-body">
- <link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
+ <div
+ v-if="status.card && !hideSubjectStatus && !noHeading"
+ class="link-preview media-body"
+ >
+ <link-preview
+ :card="status.card"
+ :size="attachmentSize"
+ :nsfw="nsfwClickthrough"
+ />
</div>
<transition name="fade">
- <div class="favs-repeated-users" v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0">
+ <div
+ v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
+ class="favs-repeated-users"
+ >
<div class="stats">
- <div class="stat-count" v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0">
+ <div
+ v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0"
+ class="stat-count"
+ >
<a class="stat-title">{{ $t('status.repeats') }}</a>
- <div class="stat-number">{{ statusFromGlobalRepository.rebloggedBy.length }}</div>
+ <div class="stat-number">
+ {{ statusFromGlobalRepository.rebloggedBy.length }}
+ </div>
</div>
- <div class="stat-count" v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0">
+ <div
+ v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0"
+ class="stat-count"
+ >
<a class="stat-title">{{ $t('status.favorites') }}</a>
- <div class="stat-number">{{ statusFromGlobalRepository.favoritedBy.length }}</div>
+ <div class="stat-number">
+ {{ statusFromGlobalRepository.favoritedBy.length }}
+ </div>
</div>
<div class="avatar-row">
- <AvatarList :users="combinedFavsAndRepeatsUsers"></AvatarList>
+ <AvatarList :users="combinedFavsAndRepeatsUsers" />
</div>
</div>
</div>
</transition>
- <div v-if="!noHeading && !isPreview" class='status-actions media-body'>
+ <div
+ v-if="!noHeading && !isPreview"
+ class="status-actions media-body"
+ >
<div>
- <i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'button-icon-active': replying}" v-if="loggedIn"/>
- <i class="button-icon button-icon-disabled icon-reply" :title="$t('tool_tip.reply')" v-else />
- <span v-if="status.replies_count > 0">{{status.replies_count}}</span>
+ <i
+ v-if="loggedIn"
+ class="button-icon icon-reply"
+ :title="$t('tool_tip.reply')"
+ :class="{'button-icon-active': replying}"
+ @click.prevent="toggleReplying"
+ />
+ <i
+ v-else
+ class="button-icon button-icon-disabled icon-reply"
+ :title="$t('tool_tip.reply')"
+ />
+ <span v-if="status.replies_count > 0">{{ status.replies_count }}</span>
</div>
- <retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
- <favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
- <extra-buttons :status="status" @onError="showError" @onSuccess="clearError"></extra-buttons>
+ <retweet-button
+ :visibility="status.visibility"
+ :logged-in="loggedIn"
+ :status="status"
+ />
+ <favorite-button
+ :logged-in="loggedIn"
+ :status="status"
+ />
+ <extra-buttons
+ :status="status"
+ @onError="showError"
+ @onSuccess="clearError"
+ />
</div>
</div>
</div>
- <div class="container" v-if="replying">
- <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :copy-message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
+ <div
+ v-if="replying"
+ class="container"
+ >
+ <post-status-form
+ class="reply-body"
+ :reply-to="status.id"
+ :attentions="status.attentions"
+ :replied-user="status.user"
+ :copy-message-scope="status.visibility"
+ :subject="replySubject"
+ @posted="toggleReplying"
+ />
</div>
</template>
</div>
+<!-- eslint-enable vue/no-v-html -->
</template>
<script src="./status.js" ></script>
@@ -449,6 +694,7 @@ $status-margin: 0.75em;
.status-content {
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
+ white-space: pre-wrap;
img, video {
max-width: 100%;
@@ -574,11 +820,12 @@ $status-margin: 0.75em;
}
.status-actions {
+ position: relative;
width: 100%;
display: flex;
margin-top: $status-margin;
- div, favorite-button {
+ > * {
max-width: 4em;
flex: 1;
}
diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js
new file mode 100644
index 00000000..a6dcded3
--- /dev/null
+++ b/src/components/sticker_picker/sticker_picker.js
@@ -0,0 +1,52 @@
+/* eslint-env browser */
+import statusPosterService from '../../services/status_poster/status_poster.service.js'
+import TabSwitcher from '../tab_switcher/tab_switcher.js'
+
+const StickerPicker = {
+ components: [
+ TabSwitcher
+ ],
+ data () {
+ return {
+ meta: {
+ stickers: []
+ },
+ path: ''
+ }
+ },
+ computed: {
+ pack () {
+ return this.$store.state.instance.stickers || []
+ }
+ },
+ methods: {
+ clear () {
+ this.meta = {
+ stickers: []
+ }
+ },
+ pick (sticker, name) {
+ const store = this.$store
+ // TODO remove this workaround by finding a way to bypass reuploads
+ fetch(sticker)
+ .then((res) => {
+ res.blob().then((blob) => {
+ var file = new File([blob], name, { mimetype: 'image/png' })
+ var formData = new FormData()
+ formData.append('file', file)
+ statusPosterService.uploadMedia({ store, formData })
+ .then((fileData) => {
+ this.$emit('uploaded', fileData)
+ this.clear()
+ }, (error) => {
+ console.warn("Can't attach sticker")
+ console.warn(error)
+ this.$emit('upload-failed', 'default')
+ })
+ })
+ })
+ }
+ }
+}
+
+export default StickerPicker
diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue
new file mode 100644
index 00000000..938204c8
--- /dev/null
+++ b/src/components/sticker_picker/sticker_picker.vue
@@ -0,0 +1,62 @@
+<template>
+ <div
+ class="sticker-picker"
+ >
+ <div
+ class="sticker-picker-panel"
+ >
+ <tab-switcher
+ :render-only-focused="true"
+ >
+ <div
+ v-for="stickerpack in pack"
+ :key="stickerpack.path"
+ :image-tooltip="stickerpack.meta.title"
+ :image="stickerpack.path + stickerpack.meta.tabIcon"
+ class="sticker-picker-content"
+ >
+ <div
+ v-for="sticker in stickerpack.meta.stickers"
+ :key="sticker"
+ class="sticker"
+ @click="pick(stickerpack.path + sticker, stickerpack.meta.title)"
+ >
+ <img
+ :src="stickerpack.path + sticker"
+ >
+ </div>
+ </div>
+ </tab-switcher>
+ </div>
+ </div>
+</template>
+
+<script src="./sticker_picker.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.sticker-picker {
+ .sticker-picker-panel {
+ display: inline-block;
+ width: 100%;
+ .sticker-picker-content {
+ max-height: 300px;
+ overflow-y: scroll;
+ overflow-x: auto;
+ .sticker {
+ display: inline-block;
+ width: 20%;
+ height: 20%;
+ img {
+ width: 100%;
+ &:hover {
+ filter: drop-shadow(0 0 5px var(--link, $fallback--link));
+ }
+ }
+ }
+ }
+ }
+}
+
+</style>
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index af824fa2..3fff63f9 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -1,7 +1,19 @@
<template>
- <div class='still-image' :class='{ animated: animated }' >
- <canvas ref="canvas" v-if="animated"></canvas>
- <img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad" @error="onError"/>
+ <div
+ class="still-image"
+ :class="{ animated: animated }"
+ >
+ <canvas
+ v-if="animated"
+ ref="canvas"
+ />
+ <img
+ ref="src"
+ :src="src"
+ :referrerpolicy="referrerpolicy"
+ @load="onLoad"
+ @error="onError"
+ >
</div>
</template>
diff --git a/src/components/style_switcher/preview.vue b/src/components/style_switcher/preview.vue
index 634f5b35..101a32bd 100644
--- a/src/components/style_switcher/preview.vue
+++ b/src/components/style_switcher/preview.vue
@@ -1,78 +1,101 @@
<template>
-<div class="panel dummy">
- <div class="panel-heading">
- <div class="title">
- {{$t('settings.style.preview.header')}}
- <span class="badge badge-notification">
- 99
+ <div class="panel dummy">
+ <div class="panel-heading">
+ <div class="title">
+ {{ $t('settings.style.preview.header') }}
+ <span class="badge badge-notification">
+ 99
+ </span>
+ </div>
+ <span class="faint">
+ {{ $t('settings.style.preview.header_faint') }}
+ </span>
+ <span class="alert error">
+ {{ $t('settings.style.preview.error') }}
</span>
+ <button class="btn">
+ {{ $t('settings.style.preview.button') }}
+ </button>
</div>
- <span class="faint">
- {{$t('settings.style.preview.header_faint')}}
- </span>
- <span class="alert error">
- {{$t('settings.style.preview.error')}}
- </span>
- <button class="btn">
- {{$t('settings.style.preview.button')}}
- </button>
- </div>
- <div class="panel-body theme-preview-content">
- <div class="post">
- <div class="avatar">
- ( ͡° ͜ʖ ͡°)
- </div>
- <div class="content">
- <h4>
- {{$t('settings.style.preview.content')}}
- </h4>
+ <div class="panel-body theme-preview-content">
+ <div class="post">
+ <div class="avatar">
+ ( ͡° ͜ʖ ͡°)
+ </div>
+ <div class="content">
+ <h4>
+ {{ $t('settings.style.preview.content') }}
+ </h4>
- <i18n path="settings.style.preview.text">
- <code style="font-family: var(--postCodeFont)">
- {{$t('settings.style.preview.mono')}}
- </code>
- <a style="color: var(--link)">
- {{$t('settings.style.preview.link')}}
- </a>
- </i18n>
+ <i18n path="settings.style.preview.text">
+ <code style="font-family: var(--postCodeFont)">
+ {{ $t('settings.style.preview.mono') }}
+ </code>
+ <a style="color: var(--link)">
+ {{ $t('settings.style.preview.link') }}
+ </a>
+ </i18n>
- <div class="icons">
- <i style="color: var(--cBlue)" class="button-icon icon-reply"/>
- <i style="color: var(--cGreen)" class="button-icon icon-retweet"/>
- <i style="color: var(--cOrange)" class="button-icon icon-star"/>
- <i style="color: var(--cRed)" class="button-icon icon-cancel"/>
+ <div class="icons">
+ <i
+ style="color: var(--cBlue)"
+ class="button-icon icon-reply"
+ />
+ <i
+ style="color: var(--cGreen)"
+ class="button-icon icon-retweet"
+ />
+ <i
+ style="color: var(--cOrange)"
+ class="button-icon icon-star"
+ />
+ <i
+ style="color: var(--cRed)"
+ class="button-icon icon-cancel"
+ />
+ </div>
</div>
</div>
- </div>
- <div class="after-post">
- <div class="avatar-alt">
- :^)
- </div>
- <div class="content">
- <i18n path="settings.style.preview.fine_print" tag="span" class="faint">
- <a style="color: var(--faintLink)">
- {{$t('settings.style.preview.faint_link')}}
- </a>
- </i18n>
+ <div class="after-post">
+ <div class="avatar-alt">
+ :^)
+ </div>
+ <div class="content">
+ <i18n
+ path="settings.style.preview.fine_print"
+ tag="span"
+ class="faint"
+ >
+ <a style="color: var(--faintLink)">
+ {{ $t('settings.style.preview.faint_link') }}
+ </a>
+ </i18n>
+ </div>
</div>
- </div>
- <div class="separator"></div>
-
- <span class="alert error">
- {{$t('settings.style.preview.error')}}
- </span>
- <input :value="$t('settings.style.preview.input')" type="text">
+ <div class="separator" />
- <div class="actions">
- <span class="checkbox">
- <input checked="very yes" type="checkbox" id="preview_checkbox">
- <label for="preview_checkbox">{{$t('settings.style.preview.checkbox')}}</label>
+ <span class="alert error">
+ {{ $t('settings.style.preview.error') }}
</span>
- <button class="btn">
- {{$t('settings.style.preview.button')}}
- </button>
+ <input
+ :value="$t('settings.style.preview.input')"
+ type="text"
+ >
+
+ <div class="actions">
+ <span class="checkbox">
+ <input
+ id="preview_checkbox"
+ checked="very yes"
+ type="checkbox"
+ >
+ <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label>
+ </span>
+ <button class="btn">
+ {{ $t('settings.style.preview.button') }}
+ </button>
+ </div>
</div>
</div>
-</div>
</template>
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 84963c81..d24394a4 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -1,274 +1,593 @@
<template>
-<div class="style-switcher">
- <div class="presets-container">
- <div class="save-load">
- <export-import
- :exportObject='exportedTheme'
- :exportLabel='$t("settings.export_theme")'
- :importLabel='$t("settings.import_theme")'
- :importFailedText='$t("settings.invalid_theme_imported")'
- :onImport='onImport'
- :validator='importValidator'>
- <template slot="before">
- <div class="presets">
- {{$t('settings.presets')}}
- <label for="preset-switcher" class='select'>
- <select id="preset-switcher" v-model="selected" class="preset-switcher">
- <option v-for="style in availableStyles"
- :value="style"
- :style="{
- backgroundColor: style[1] || style.theme.colors.bg,
- color: style[3] || style.theme.colors.text
- }">
- {{style[0] || style.name}}
- </option>
- </select>
- <i class="icon-down-open"/>
- </label>
- </div>
- </template>
- </export-import>
- </div>
- <div class="save-load-options">
- <span class="keep-option">
- <input
- id="keep-color"
- type="checkbox"
- v-model="keepColor">
- <label for="keep-color">{{$t('settings.style.switcher.keep_color')}}</label>
- </span>
- <span class="keep-option">
- <input
- id="keep-shadows"
- type="checkbox"
- v-model="keepShadows">
- <label for="keep-shadows">{{$t('settings.style.switcher.keep_shadows')}}</label>
- </span>
- <span class="keep-option">
- <input
- id="keep-opacity"
- type="checkbox"
- v-model="keepOpacity">
- <label for="keep-opacity">{{$t('settings.style.switcher.keep_opacity')}}</label>
- </span>
- <span class="keep-option">
- <input
- id="keep-roundness"
- type="checkbox"
- v-model="keepRoundness">
- <label for="keep-roundness">{{$t('settings.style.switcher.keep_roundness')}}</label>
- </span>
- <span class="keep-option">
- <input
- id="keep-fonts"
- type="checkbox"
- v-model="keepFonts">
- <label for="keep-fonts">{{$t('settings.style.switcher.keep_fonts')}}</label>
- </span>
- <p>{{$t('settings.style.switcher.save_load_hint')}}</p>
+ <div class="style-switcher">
+ <div class="presets-container">
+ <div class="save-load">
+ <export-import
+ :export-object="exportedTheme"
+ :export-label="$t(&quot;settings.export_theme&quot;)"
+ :import-label="$t(&quot;settings.import_theme&quot;)"
+ :import-failed-text="$t(&quot;settings.invalid_theme_imported&quot;)"
+ :on-import="onImport"
+ :validator="importValidator"
+ >
+ <template slot="before">
+ <div class="presets">
+ {{ $t('settings.presets') }}
+ <label
+ for="preset-switcher"
+ class="select"
+ >
+ <select
+ id="preset-switcher"
+ v-model="selected"
+ class="preset-switcher"
+ >
+ <option
+ v-for="style in availableStyles"
+ :key="style.name"
+ :value="style"
+ :style="{
+ backgroundColor: style[1] || style.theme.colors.bg,
+ color: style[3] || style.theme.colors.text
+ }"
+ >
+ {{ style[0] || style.name }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
+ </template>
+ </export-import>
+ </div>
+ <div class="save-load-options">
+ <span class="keep-option">
+ <input
+ id="keep-color"
+ v-model="keepColor"
+ type="checkbox"
+ >
+ <label for="keep-color">{{ $t('settings.style.switcher.keep_color') }}</label>
+ </span>
+ <span class="keep-option">
+ <input
+ id="keep-shadows"
+ v-model="keepShadows"
+ type="checkbox"
+ >
+ <label for="keep-shadows">{{ $t('settings.style.switcher.keep_shadows') }}</label>
+ </span>
+ <span class="keep-option">
+ <input
+ id="keep-opacity"
+ v-model="keepOpacity"
+ type="checkbox"
+ >
+ <label for="keep-opacity">{{ $t('settings.style.switcher.keep_opacity') }}</label>
+ </span>
+ <span class="keep-option">
+ <input
+ id="keep-roundness"
+ v-model="keepRoundness"
+ type="checkbox"
+ >
+ <label for="keep-roundness">{{ $t('settings.style.switcher.keep_roundness') }}</label>
+ </span>
+ <span class="keep-option">
+ <input
+ id="keep-fonts"
+ v-model="keepFonts"
+ type="checkbox"
+ >
+ <label for="keep-fonts">{{ $t('settings.style.switcher.keep_fonts') }}</label>
+ </span>
+ <p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
+ </div>
</div>
- </div>
- <div class="preview-container">
- <preview :style="previewRules"/>
- </div>
+ <div class="preview-container">
+ <preview :style="previewRules" />
+ </div>
- <keep-alive>
- <tab-switcher key="style-tweak">
- <div :label="$t('settings.style.common_colors._tab_label')" class="color-container">
- <div class="tab-header">
- <p>{{$t('settings.theme_help')}}</p>
- <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
- <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
- </div>
- <p>{{$t('settings.theme_help_v2_1')}}</p>
- <h4>{{ $t('settings.style.common_colors.main') }}</h4>
- <div class="color-item">
- <ColorInput name="bgColor" v-model="bgColorLocal" :label="$t('settings.background')"/>
- <OpacityInput name="bgOpacity" v-model="bgOpacityLocal" :fallback="previewTheme.opacity.bg || 1"/>
- <ColorInput name="textColor" v-model="textColorLocal" :label="$t('settings.text')"/>
- <ContrastRatio :contrast="previewContrast.bgText"/>
- <ColorInput name="linkColor" v-model="linkColorLocal" :label="$t('settings.links')"/>
- <ContrastRatio :contrast="previewContrast.bgLink"/>
- </div>
- <div class="color-item">
- <ColorInput name="fgColor" v-model="fgColorLocal" :label="$t('settings.foreground')"/>
- <ColorInput name="fgTextColor" v-model="fgTextColorLocal" :label="$t('settings.text')" :fallback="previewTheme.colors.fgText"/>
- <ColorInput name="fgLinkColor" v-model="fgLinkColorLocal" :label="$t('settings.links')" :fallback="previewTheme.colors.fgLink"/>
- <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
- </div>
- <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
- <div class="color-item">
- <ColorInput name="cRedColor" v-model="cRedColorLocal" :label="$t('settings.cRed')"/>
- <ContrastRatio :contrast="previewContrast.bgRed"/>
- <ColorInput name="cBlueColor" v-model="cBlueColorLocal" :label="$t('settings.cBlue')"/>
- <ContrastRatio :contrast="previewContrast.bgBlue"/>
- </div>
- <div class="color-item">
- <ColorInput name="cGreenColor" v-model="cGreenColorLocal" :label="$t('settings.cGreen')"/>
- <ContrastRatio :contrast="previewContrast.bgGreen"/>
- <ColorInput name="cOrangeColor" v-model="cOrangeColorLocal" :label="$t('settings.cOrange')"/>
- <ContrastRatio :contrast="previewContrast.bgOrange"/>
+ <keep-alive>
+ <tab-switcher key="style-tweak">
+ <div
+ :label="$t('settings.style.common_colors._tab_label')"
+ class="color-container"
+ >
+ <div class="tab-header">
+ <p>{{ $t('settings.theme_help') }}</p>
+ <button
+ class="btn"
+ @click="clearOpacity"
+ >
+ {{ $t('settings.style.switcher.clear_opacity') }}
+ </button>
+ <button
+ class="btn"
+ @click="clearV1"
+ >
+ {{ $t('settings.style.switcher.clear_all') }}
+ </button>
+ </div>
+ <p>{{ $t('settings.theme_help_v2_1') }}</p>
+ <h4>{{ $t('settings.style.common_colors.main') }}</h4>
+ <div class="color-item">
+ <ColorInput
+ v-model="bgColorLocal"
+ name="bgColor"
+ :label="$t('settings.background')"
+ />
+ <OpacityInput
+ v-model="bgOpacityLocal"
+ name="bgOpacity"
+ :fallback="previewTheme.opacity.bg || 1"
+ />
+ <ColorInput
+ v-model="textColorLocal"
+ name="textColor"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.bgText" />
+ <ColorInput
+ v-model="linkColorLocal"
+ name="linkColor"
+ :label="$t('settings.links')"
+ />
+ <ContrastRatio :contrast="previewContrast.bgLink" />
+ </div>
+ <div class="color-item">
+ <ColorInput
+ v-model="fgColorLocal"
+ name="fgColor"
+ :label="$t('settings.foreground')"
+ />
+ <ColorInput
+ v-model="fgTextColorLocal"
+ name="fgTextColor"
+ :label="$t('settings.text')"
+ :fallback="previewTheme.colors.fgText"
+ />
+ <ColorInput
+ v-model="fgLinkColorLocal"
+ name="fgLinkColor"
+ :label="$t('settings.links')"
+ :fallback="previewTheme.colors.fgLink"
+ />
+ <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p>
+ </div>
+ <h4>{{ $t('settings.style.common_colors.rgbo') }}</h4>
+ <div class="color-item">
+ <ColorInput
+ v-model="cRedColorLocal"
+ name="cRedColor"
+ :label="$t('settings.cRed')"
+ />
+ <ContrastRatio :contrast="previewContrast.bgRed" />
+ <ColorInput
+ v-model="cBlueColorLocal"
+ name="cBlueColor"
+ :label="$t('settings.cBlue')"
+ />
+ <ContrastRatio :contrast="previewContrast.bgBlue" />
+ </div>
+ <div class="color-item">
+ <ColorInput
+ v-model="cGreenColorLocal"
+ name="cGreenColor"
+ :label="$t('settings.cGreen')"
+ />
+ <ContrastRatio :contrast="previewContrast.bgGreen" />
+ <ColorInput
+ v-model="cOrangeColorLocal"
+ name="cOrangeColor"
+ :label="$t('settings.cOrange')"
+ />
+ <ContrastRatio :contrast="previewContrast.bgOrange" />
+ </div>
+ <p>{{ $t('settings.theme_help_v2_2') }}</p>
</div>
- <p>{{$t('settings.theme_help_v2_2')}}</p>
- </div>
- <div :label="$t('settings.style.advanced_colors._tab_label')" class="color-container">
- <div class="tab-header">
- <p>{{$t('settings.theme_help')}}</p>
- <button class="btn" @click="clearOpacity">{{$t('settings.style.switcher.clear_opacity')}}</button>
- <button class="btn" @click="clearV1">{{$t('settings.style.switcher.clear_all')}}</button>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
- <ColorInput name="alertError" v-model="alertErrorColorLocal" :label="$t('settings.style.advanced_colors.alert_error')" :fallback="previewTheme.colors.alertError"/>
- <ContrastRatio :contrast="previewContrast.alertError"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
- <ColorInput name="badgeNotification" v-model="badgeNotificationColorLocal" :label="$t('settings.style.advanced_colors.badge_notification')" :fallback="previewTheme.colors.badgeNotification"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
- <ColorInput name="panelColor" v-model="panelColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
- <OpacityInput name="panelOpacity" v-model="panelOpacityLocal" :fallback="previewTheme.opacity.panel || 1"/>
- <ColorInput name="panelTextColor" v-model="panelTextColorLocal" :fallback="previewTheme.colors.panelText" :label="$t('settings.text')"/>
- <ContrastRatio :contrast="previewContrast.panelText" large="1"/>
- <ColorInput name="panelLinkColor" v-model="panelLinkColorLocal" :fallback="previewTheme.colors.panelLink" :label="$t('settings.links')"/>
- <ContrastRatio :contrast="previewContrast.panelLink" large="1"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
- <ColorInput name="topBarColor" v-model="topBarColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
- <ColorInput name="topBarTextColor" v-model="topBarTextColorLocal" :fallback="previewTheme.colors.topBarText" :label="$t('settings.text')"/>
- <ContrastRatio :contrast="previewContrast.topBarText"/>
- <ColorInput name="topBarLinkColor" v-model="topBarLinkColorLocal" :fallback="previewTheme.colors.topBarLink" :label="$t('settings.links')"/>
- <ContrastRatio :contrast="previewContrast.topBarLink"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
- <ColorInput name="inputColor" v-model="inputColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
- <OpacityInput name="inputOpacity" v-model="inputOpacityLocal" :fallback="previewTheme.opacity.input || 1"/>
- <ColorInput name="inputTextColor" v-model="inputTextColorLocal" :fallback="previewTheme.colors.inputText" :label="$t('settings.text')"/>
- <ContrastRatio :contrast="previewContrast.inputText"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
- <ColorInput name="btnColor" v-model="btnColorLocal" :fallback="fgColorLocal" :label="$t('settings.background')"/>
- <OpacityInput name="btnOpacity" v-model="btnOpacityLocal" :fallback="previewTheme.opacity.btn || 1"/>
- <ColorInput name="btnTextColor" v-model="btnTextColorLocal" :fallback="previewTheme.colors.btnText" :label="$t('settings.text')"/>
- <ContrastRatio :contrast="previewContrast.btnText"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
- <ColorInput name="borderColor" v-model="borderColorLocal" :fallback="previewTheme.colors.border" :label="$t('settings.style.common.color')"/>
- <OpacityInput name="borderOpacity" v-model="borderOpacityLocal" :fallback="previewTheme.opacity.border || 1"/>
- </div>
- <div class="color-item">
- <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
- <ColorInput name="faintColor" v-model="faintColorLocal" :fallback="previewTheme.colors.faint || 1" :label="$t('settings.text')"/>
- <ColorInput name="faintLinkColor" v-model="faintLinkColorLocal" :fallback="previewTheme.colors.faintLink" :label="$t('settings.links')"/>
- <ColorInput name="panelFaintColor" v-model="panelFaintColorLocal" :fallback="previewTheme.colors.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')"/>
- <OpacityInput name="faintOpacity" v-model="faintOpacityLocal" :fallback="previewTheme.opacity.faint || 0.5"/>
+ <div
+ :label="$t('settings.style.advanced_colors._tab_label')"
+ class="color-container"
+ >
+ <div class="tab-header">
+ <p>{{ $t('settings.theme_help') }}</p>
+ <button
+ class="btn"
+ @click="clearOpacity"
+ >
+ {{ $t('settings.style.switcher.clear_opacity') }}
+ </button>
+ <button
+ class="btn"
+ @click="clearV1"
+ >
+ {{ $t('settings.style.switcher.clear_all') }}
+ </button>
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.alert') }}</h4>
+ <ColorInput
+ v-model="alertErrorColorLocal"
+ name="alertError"
+ :label="$t('settings.style.advanced_colors.alert_error')"
+ :fallback="previewTheme.colors.alertError"
+ />
+ <ContrastRatio :contrast="previewContrast.alertError" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.badge') }}</h4>
+ <ColorInput
+ v-model="badgeNotificationColorLocal"
+ name="badgeNotification"
+ :label="$t('settings.style.advanced_colors.badge_notification')"
+ :fallback="previewTheme.colors.badgeNotification"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.panel_header') }}</h4>
+ <ColorInput
+ v-model="panelColorLocal"
+ name="panelColor"
+ :fallback="fgColorLocal"
+ :label="$t('settings.background')"
+ />
+ <OpacityInput
+ v-model="panelOpacityLocal"
+ name="panelOpacity"
+ :fallback="previewTheme.opacity.panel || 1"
+ />
+ <ColorInput
+ v-model="panelTextColorLocal"
+ name="panelTextColor"
+ :fallback="previewTheme.colors.panelText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.panelText"
+ large="1"
+ />
+ <ColorInput
+ v-model="panelLinkColorLocal"
+ name="panelLinkColor"
+ :fallback="previewTheme.colors.panelLink"
+ :label="$t('settings.links')"
+ />
+ <ContrastRatio
+ :contrast="previewContrast.panelLink"
+ large="1"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.top_bar') }}</h4>
+ <ColorInput
+ v-model="topBarColorLocal"
+ name="topBarColor"
+ :fallback="fgColorLocal"
+ :label="$t('settings.background')"
+ />
+ <ColorInput
+ v-model="topBarTextColorLocal"
+ name="topBarTextColor"
+ :fallback="previewTheme.colors.topBarText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.topBarText" />
+ <ColorInput
+ v-model="topBarLinkColorLocal"
+ name="topBarLinkColor"
+ :fallback="previewTheme.colors.topBarLink"
+ :label="$t('settings.links')"
+ />
+ <ContrastRatio :contrast="previewContrast.topBarLink" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.inputs') }}</h4>
+ <ColorInput
+ v-model="inputColorLocal"
+ name="inputColor"
+ :fallback="fgColorLocal"
+ :label="$t('settings.background')"
+ />
+ <OpacityInput
+ v-model="inputOpacityLocal"
+ name="inputOpacity"
+ :fallback="previewTheme.opacity.input || 1"
+ />
+ <ColorInput
+ v-model="inputTextColorLocal"
+ name="inputTextColor"
+ :fallback="previewTheme.colors.inputText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.inputText" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.buttons') }}</h4>
+ <ColorInput
+ v-model="btnColorLocal"
+ name="btnColor"
+ :fallback="fgColorLocal"
+ :label="$t('settings.background')"
+ />
+ <OpacityInput
+ v-model="btnOpacityLocal"
+ name="btnOpacity"
+ :fallback="previewTheme.opacity.btn || 1"
+ />
+ <ColorInput
+ v-model="btnTextColorLocal"
+ name="btnTextColor"
+ :fallback="previewTheme.colors.btnText"
+ :label="$t('settings.text')"
+ />
+ <ContrastRatio :contrast="previewContrast.btnText" />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.borders') }}</h4>
+ <ColorInput
+ v-model="borderColorLocal"
+ name="borderColor"
+ :fallback="previewTheme.colors.border"
+ :label="$t('settings.style.common.color')"
+ />
+ <OpacityInput
+ v-model="borderOpacityLocal"
+ name="borderOpacity"
+ :fallback="previewTheme.opacity.border || 1"
+ />
+ </div>
+ <div class="color-item">
+ <h4>{{ $t('settings.style.advanced_colors.faint_text') }}</h4>
+ <ColorInput
+ v-model="faintColorLocal"
+ name="faintColor"
+ :fallback="previewTheme.colors.faint || 1"
+ :label="$t('settings.text')"
+ />
+ <ColorInput
+ v-model="faintLinkColorLocal"
+ name="faintLinkColor"
+ :fallback="previewTheme.colors.faintLink"
+ :label="$t('settings.links')"
+ />
+ <ColorInput
+ v-model="panelFaintColorLocal"
+ name="panelFaintColor"
+ :fallback="previewTheme.colors.panelFaint"
+ :label="$t('settings.style.advanced_colors.panel_header')"
+ />
+ <OpacityInput
+ v-model="faintOpacityLocal"
+ name="faintOpacity"
+ :fallback="previewTheme.opacity.faint || 0.5"
+ />
+ </div>
</div>
- </div>
- <div :label="$t('settings.style.radii._tab_label')" class="radius-container">
- <div class="tab-header">
- <p>{{$t('settings.radii_help')}}</p>
- <button class="btn" @click="clearRoundness">{{$t('settings.style.switcher.clear_all')}}</button>
+ <div
+ :label="$t('settings.style.radii._tab_label')"
+ class="radius-container"
+ >
+ <div class="tab-header">
+ <p>{{ $t('settings.radii_help') }}</p>
+ <button
+ class="btn"
+ @click="clearRoundness"
+ >
+ {{ $t('settings.style.switcher.clear_all') }}
+ </button>
+ </div>
+ <RangeInput
+ v-model="btnRadiusLocal"
+ name="btnRadius"
+ :label="$t('settings.btnRadius')"
+ :fallback="previewTheme.radii.btn"
+ max="16"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="inputRadiusLocal"
+ name="inputRadius"
+ :label="$t('settings.inputRadius')"
+ :fallback="previewTheme.radii.input"
+ max="9"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="checkboxRadiusLocal"
+ name="checkboxRadius"
+ :label="$t('settings.checkboxRadius')"
+ :fallback="previewTheme.radii.checkbox"
+ max="16"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="panelRadiusLocal"
+ name="panelRadius"
+ :label="$t('settings.panelRadius')"
+ :fallback="previewTheme.radii.panel"
+ max="50"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="avatarRadiusLocal"
+ name="avatarRadius"
+ :label="$t('settings.avatarRadius')"
+ :fallback="previewTheme.radii.avatar"
+ max="28"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="avatarAltRadiusLocal"
+ name="avatarAltRadius"
+ :label="$t('settings.avatarAltRadius')"
+ :fallback="previewTheme.radii.avatarAlt"
+ max="28"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="attachmentRadiusLocal"
+ name="attachmentRadius"
+ :label="$t('settings.attachmentRadius')"
+ :fallback="previewTheme.radii.attachment"
+ max="50"
+ hard-min="0"
+ />
+ <RangeInput
+ v-model="tooltipRadiusLocal"
+ name="tooltipRadius"
+ :label="$t('settings.tooltipRadius')"
+ :fallback="previewTheme.radii.tooltip"
+ max="50"
+ hard-min="0"
+ />
</div>
- <RangeInput name="btnRadius" :label="$t('settings.btnRadius')" v-model="btnRadiusLocal" :fallback="previewTheme.radii.btn" max="16" hardMin="0"/>
- <RangeInput name="inputRadius" :label="$t('settings.inputRadius')" v-model="inputRadiusLocal" :fallback="previewTheme.radii.input" max="9" hardMin="0"/>
- <RangeInput name="checkboxRadius" :label="$t('settings.checkboxRadius')" v-model="checkboxRadiusLocal" :fallback="previewTheme.radii.checkbox" max="16" hardMin="0"/>
- <RangeInput name="panelRadius" :label="$t('settings.panelRadius')" v-model="panelRadiusLocal" :fallback="previewTheme.radii.panel" max="50" hardMin="0"/>
- <RangeInput name="avatarRadius" :label="$t('settings.avatarRadius')" v-model="avatarRadiusLocal" :fallback="previewTheme.radii.avatar" max="28" hardMin="0"/>
- <RangeInput name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" v-model="avatarAltRadiusLocal" :fallback="previewTheme.radii.avatarAlt" max="28" hardMin="0"/>
- <RangeInput name="attachmentRadius" :label="$t('settings.attachmentRadius')" v-model="attachmentRadiusLocal" :fallback="previewTheme.radii.attachment" max="50" hardMin="0"/>
- <RangeInput name="tooltipRadius" :label="$t('settings.tooltipRadius')" v-model="tooltipRadiusLocal" :fallback="previewTheme.radii.tooltip" max="50" hardMin="0"/>
- </div>
- <div :label="$t('settings.style.shadows._tab_label')" class="shadow-container">
- <div class="tab-header shadow-selector">
- <div class="select-container">
- {{$t('settings.style.shadows.component')}}
- <label for="shadow-switcher" class="select">
- <select id="shadow-switcher" v-model="shadowSelected" class="shadow-switcher">
- <option v-for="shadow in shadowsAvailable"
- :value="shadow">
- {{$t('settings.style.shadows.components.' + shadow)}}
- </option>
- </select>
- <i class="icon-down-open"/>
- </label>
+ <div
+ :label="$t('settings.style.shadows._tab_label')"
+ class="shadow-container"
+ >
+ <div class="tab-header shadow-selector">
+ <div class="select-container">
+ {{ $t('settings.style.shadows.component') }}
+ <label
+ for="shadow-switcher"
+ class="select"
+ >
+ <select
+ id="shadow-switcher"
+ v-model="shadowSelected"
+ class="shadow-switcher"
+ >
+ <option
+ v-for="shadow in shadowsAvailable"
+ :key="shadow"
+ :value="shadow"
+ >
+ {{ $t('settings.style.shadows.components.' + shadow) }}
+ </option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
+ <div class="override">
+ <label
+ for="override"
+ class="label"
+ >
+ {{ $t('settings.style.shadows.override') }}
+ </label>
+ <input
+ id="override"
+ v-model="currentShadowOverriden"
+ name="override"
+ class="input-override"
+ type="checkbox"
+ >
+ <label
+ class="checkbox-label"
+ for="override"
+ />
+ </div>
+ <button
+ class="btn"
+ @click="clearShadows"
+ >
+ {{ $t('settings.style.switcher.clear_all') }}
+ </button>
</div>
- <div class="override">
- <label for="override" class="label">
- {{$t('settings.style.shadows.override')}}
- </label>
- <input
- v-model="currentShadowOverriden"
- name="override"
- id="override"
- class="input-override"
- type="checkbox">
- <label class="checkbox-label" for="override"></label>
+ <shadow-control
+ v-model="currentShadow"
+ :ready="!!currentShadowFallback"
+ :fallback="currentShadowFallback"
+ />
+ <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
+ <i18n
+ path="settings.style.shadows.filter_hint.always_drop_shadow"
+ tag="p"
+ >
+ <code>filter: drop-shadow()</code>
+ </i18n>
+ <p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
+ <i18n
+ path="settings.style.shadows.filter_hint.drop_shadow_syntax"
+ tag="p"
+ >
+ <code>drop-shadow</code>
+ <code>spread-radius</code>
+ <code>inset</code>
+ </i18n>
+ <i18n
+ path="settings.style.shadows.filter_hint.inset_classic"
+ tag="p"
+ >
+ <code>box-shadow</code>
+ </i18n>
+ <p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
</div>
- <button class="btn" @click="clearShadows">{{$t('settings.style.switcher.clear_all')}}</button>
</div>
- <shadow-control :ready="!!currentShadowFallback" :fallback="currentShadowFallback" v-model="currentShadow"/>
- <div v-if="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'">
- <i18n path="settings.style.shadows.filter_hint.always_drop_shadow" tag="p">
- <code>filter: drop-shadow()</code>
- </i18n>
- <p>{{$t('settings.style.shadows.filter_hint.avatar_inset')}}</p>
- <i18n path="settings.style.shadows.filter_hint.drop_shadow_syntax" tag="p">
- <code>drop-shadow</code>
- <code>spread-radius</code>
- <code>inset</code>
- </i18n>
- <i18n path="settings.style.shadows.filter_hint.inset_classic" tag="p">
- <code>box-shadow</code>
- </i18n>
- <p>{{$t('settings.style.shadows.filter_hint.spread_zero')}}</p>
- </div>
- </div>
- <div :label="$t('settings.style.fonts._tab_label')" class="fonts-container">
- <div class="tab-header">
- <p>{{$t('settings.style.fonts.help')}}</p>
- <button class="btn" @click="clearFonts">{{$t('settings.style.switcher.clear_all')}}</button>
+ <div
+ :label="$t('settings.style.fonts._tab_label')"
+ class="fonts-container"
+ >
+ <div class="tab-header">
+ <p>{{ $t('settings.style.fonts.help') }}</p>
+ <button
+ class="btn"
+ @click="clearFonts"
+ >
+ {{ $t('settings.style.switcher.clear_all') }}
+ </button>
+ </div>
+ <FontControl
+ v-model="fontsLocal.interface"
+ name="ui"
+ :label="$t('settings.style.fonts.components.interface')"
+ :fallback="previewTheme.fonts.interface"
+ no-inherit="1"
+ />
+ <FontControl
+ v-model="fontsLocal.input"
+ name="input"
+ :label="$t('settings.style.fonts.components.input')"
+ :fallback="previewTheme.fonts.input"
+ />
+ <FontControl
+ v-model="fontsLocal.post"
+ name="post"
+ :label="$t('settings.style.fonts.components.post')"
+ :fallback="previewTheme.fonts.post"
+ />
+ <FontControl
+ v-model="fontsLocal.postCode"
+ name="postCode"
+ :label="$t('settings.style.fonts.components.postCode')"
+ :fallback="previewTheme.fonts.postCode"
+ />
</div>
- <FontControl
- name="ui"
- v-model="fontsLocal.interface"
- :label="$t('settings.style.fonts.components.interface')"
- :fallback="previewTheme.fonts.interface"
- no-inherit="1"/>
- <FontControl
- name="input"
- v-model="fontsLocal.input"
- :label="$t('settings.style.fonts.components.input')"
- :fallback="previewTheme.fonts.input"/>
- <FontControl
- name="post"
- v-model="fontsLocal.post"
- :label="$t('settings.style.fonts.components.post')"
- :fallback="previewTheme.fonts.post"/>
- <FontControl
- name="postCode"
- v-model="fontsLocal.postCode"
- :label="$t('settings.style.fonts.components.postCode')"
- :fallback="previewTheme.fonts.postCode"/>
- </div>
- </tab-switcher>
- </keep-alive>
+ </tab-switcher>
+ </keep-alive>
- <div class="apply-container">
- <button class="btn submit" :disabled="!themeValid" @click="setCustomTheme">{{$t('general.apply')}}</button>
- <button class="btn" @click="clearAll">{{$t('settings.style.switcher.reset')}}</button>
+ <div class="apply-container">
+ <button
+ class="btn submit"
+ :disabled="!themeValid"
+ @click="setCustomTheme"
+ >
+ {{ $t('general.apply') }}
+ </button>
+ <button
+ class="btn"
+ @click="clearAll"
+ >
+ {{ $t('settings.style.switcher.reset') }}
+ </button>
+ </div>
</div>
-</div>
</template>
<script src="./style_switcher.js"></script>
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index c949b458..08d5d08f 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,19 +4,19 @@ import './tab_switcher.scss'
export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
- props: ['renderOnlyFocused', 'onSwitch'],
+ props: ['renderOnlyFocused', 'onSwitch', 'activeTab'],
data () {
return {
active: this.$slots.default.findIndex(_ => _.tag)
}
},
- methods: {
- activateTab (index, dataset) {
- return () => {
- if (typeof this.onSwitch === 'function') {
- this.onSwitch.call(null, index, this.$slots.default[index].elm.dataset)
- }
- this.active = index
+ computed: {
+ activeIndex () {
+ // In case of controlled component
+ if (this.activeTab) {
+ return this.$slots.default.findIndex(slot => this.activeTab === slot.key)
+ } else {
+ return this.active
}
}
},
@@ -26,32 +26,54 @@ export default Vue.component('tab-switcher', {
this.active = this.$slots.default.findIndex(_ => _.tag)
}
},
+ methods: {
+ activateTab (index) {
+ return () => {
+ if (typeof this.onSwitch === 'function') {
+ this.onSwitch.call(null, this.$slots.default[index].key)
+ }
+ this.active = index
+ }
+ }
+ },
render (h) {
const tabs = this.$slots.default
- .map((slot, index) => {
- if (!slot.tag) return
- const classesTab = ['tab']
- const classesWrapper = ['tab-wrapper']
-
- if (index === this.active) {
- classesTab.push('active')
- classesWrapper.push('active')
- }
+ .map((slot, index) => {
+ if (!slot.tag) return
+ const classesTab = ['tab']
+ const classesWrapper = ['tab-wrapper']
- return (
- <div class={ classesWrapper.join(' ')}>
- <button
- disabled={slot.data.attrs.disabled}
- onClick={this.activateTab(index)}
- class={classesTab.join(' ')}>
- {slot.data.attrs.label}</button>
- </div>
- )
- })
+ if (this.activeIndex === index) {
+ classesTab.push('active')
+ classesWrapper.push('active')
+ }
+ if (slot.data.attrs.image) {
+ return (
+ <div class={classesWrapper.join(' ')}>
+ <button
+ disabled={slot.data.attrs.disabled}
+ onClick={this.activateTab(index)}
+ class={classesTab.join(' ')}>
+ <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/>
+ {slot.data.attrs.label ? '' : slot.data.attrs.label}
+ </button>
+ </div>
+ )
+ }
+ return (
+ <div class={classesWrapper.join(' ')}>
+ <button
+ disabled={slot.data.attrs.disabled}
+ onClick={this.activateTab(index)}
+ class={classesTab.join(' ')}>
+ {slot.data.attrs.label}</button>
+ </div>
+ )
+ })
const contents = this.$slots.default.map((slot, index) => {
if (!slot.tag) return
- const active = index === this.active
+ const active = this.activeIndex === index
if (this.renderOnlyFocused) {
return active
? <div class="active">{slot}</div>
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index f7449439..4eeb42e0 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -53,6 +53,12 @@
background: transparent;
z-index: 5;
}
+
+ img {
+ max-height: 26px;
+ vertical-align: top;
+ margin-top: -5px;
+ }
}
&:not(.active) {
diff --git a/src/components/tag_timeline/tag_timeline.vue b/src/components/tag_timeline/tag_timeline.vue
index 62bb579a..ace96c3f 100644
--- a/src/components/tag_timeline/tag_timeline.vue
+++ b/src/components/tag_timeline/tag_timeline.vue
@@ -1,5 +1,10 @@
<template>
- <Timeline :title="tag" :timeline="timeline" :timeline-name="'tag'" :tag="tag" />
+ <Timeline
+ :title="tag"
+ :timeline="timeline"
+ :timeline-name="'tag'"
+ :tag="tag"
+ />
</template>
-<script src='./tag_timeline.js'></script> \ No newline at end of file
+<script src='./tag_timeline.js'></script>
diff --git a/src/components/terms_of_service_panel/terms_of_service_panel.vue b/src/components/terms_of_service_panel/terms_of_service_panel.vue
index eb0f2527..63dc58b8 100644
--- a/src/components/terms_of_service_panel/terms_of_service_panel.vue
+++ b/src/components/terms_of_service_panel/terms_of_service_panel.vue
@@ -2,8 +2,12 @@
<div>
<div class="panel panel-default">
<div class="panel-body">
- <div v-html="content" class="tos-content">
- </div>
+ <!-- eslint-disable vue/no-v-html -->
+ <div
+ class="tos-content"
+ v-html="content"
+ />
+ <!-- eslint-enable vue/no-v-html -->
</div>
</div>
</div>
diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue
new file mode 100644
index 00000000..6df0524d
--- /dev/null
+++ b/src/components/timeago/timeago.vue
@@ -0,0 +1,51 @@
+<template>
+ <time
+ :datetime="time"
+ :title="localeDateString"
+ >
+ {{ $t(relativeTime.key, [relativeTime.num]) }}
+ </time>
+</template>
+
+<script>
+import * as DateUtils from 'src/services/date_utils/date_utils.js'
+
+export default {
+ name: 'Timeago',
+ props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'],
+ data () {
+ return {
+ relativeTime: { key: 'time.now', num: 0 },
+ interval: null
+ }
+ },
+ computed: {
+ localeDateString () {
+ return typeof this.time === 'string'
+ ? new Date(Date.parse(this.time)).toLocaleString()
+ : this.time.toLocaleString()
+ }
+ },
+ created () {
+ this.refreshRelativeTimeObject()
+ },
+ destroyed () {
+ clearTimeout(this.interval)
+ },
+ methods: {
+ refreshRelativeTimeObject () {
+ const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1
+ this.relativeTime = this.longFormat
+ ? DateUtils.relativeTime(this.time, nowThreshold)
+ : DateUtils.relativeTimeShort(this.time, nowThreshold)
+
+ if (this.autoUpdate) {
+ this.interval = setTimeout(
+ this.refreshRelativeTimeObject,
+ 1000 * this.autoUpdate
+ )
+ }
+ }
+ }
+}
+</script>
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 19d9a9ac..8df48f7f 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,7 +1,20 @@
import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue'
-import { throttle } from 'lodash'
+import { throttle, keyBy } from 'lodash'
+
+export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
+ const ids = []
+ if (pinnedStatusIds && pinnedStatusIds.length > 0) {
+ for (let status of statuses) {
+ if (!pinnedStatusIds.includes(status.id)) {
+ break
+ }
+ ids.push(status.id)
+ }
+ }
+ return ids
+}
const Timeline = {
props: [
@@ -11,7 +24,8 @@ const Timeline = {
'userId',
'tag',
'embedded',
- 'count'
+ 'count',
+ 'pinnedStatusIds'
],
data () {
return {
@@ -39,6 +53,15 @@ const Timeline = {
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
}
+ },
+ // id map of statuses which need to be hidden in the main list due to pinning logic
+ excludedStatusIdsObject () {
+ const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds)
+ // Convert id array to object
+ return keyBy(ids)
+ },
+ pinnedStatusIdsObject () {
+ return keyBy(this.pinnedStatusIds)
}
},
components: {
@@ -78,13 +101,15 @@ const Timeline = {
},
methods: {
handleShortKey (e) {
+ // Ignore when input fields are focused
+ if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses () {
if (this.newStatusCount === 0) return
if (this.timeline.flushMarker !== 0) {
- this.$store.commit('clearTimeline', { timeline: this.timelineName })
+ this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
this.fetchOlderStatuses()
} else {
@@ -137,7 +162,7 @@ const Timeline = {
if (top < 15 &&
!this.paused &&
!(this.unfocused && this.$store.state.config.pauseOnUnfocused)
- ) {
+ ) {
this.showNewStatuses()
} else {
this.paused = true
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index e6a8d458..4ad51714 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -2,41 +2,78 @@
<div :class="classes.root">
<div :class="classes.header">
<div class="title">
- {{title}}
+ {{ title }}
</div>
- <div @click.prevent class="loadmore-error alert error" v-if="timelineError">
- {{$t('timeline.error_fetching')}}
+ <div
+ v-if="timelineError"
+ class="loadmore-error alert error"
+ @click.prevent
+ >
+ {{ $t('timeline.error_fetching') }}
</div>
- <button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
- {{$t('timeline.show_new')}}{{newStatusCountStr}}
+ <button
+ v-if="timeline.newStatusCount > 0 && !timelineError"
+ class="loadmore-button"
+ @click.prevent="showNewStatuses"
+ >
+ {{ $t('timeline.show_new') }}{{ newStatusCountStr }}
</button>
- <div @click.prevent class="loadmore-text faint" v-if="!timeline.newStatusCount > 0 && !timelineError">
- {{$t('timeline.up_to_date')}}
+ <div
+ v-if="!timeline.newStatusCount > 0 && !timelineError"
+ class="loadmore-text faint"
+ @click.prevent
+ >
+ {{ $t('timeline.up_to_date') }}
</div>
</div>
<div :class="classes.body">
<div class="timeline">
- <conversation
- v-for="status in timeline.visibleStatuses"
- class="status-fadein"
- :key="status.id"
- :statusoid="status"
- :collapsable="true"
- />
+ <template v-for="statusId in pinnedStatusIds">
+ <conversation
+ v-if="timeline.statusesObject[statusId]"
+ :key="statusId + '-pinned'"
+ class="status-fadein"
+ :statusoid="timeline.statusesObject[statusId]"
+ :collapsable="true"
+ :pinned-status-ids-object="pinnedStatusIdsObject"
+ />
+ </template>
+ <template v-for="status in timeline.visibleStatuses">
+ <conversation
+ v-if="!excludedStatusIdsObject[status.id]"
+ :key="status.id"
+ class="status-fadein"
+ :statusoid="status"
+ :collapsable="true"
+ />
+ </template>
</div>
</div>
<div :class="classes.footer">
- <div v-if="count===0" class="new-status-notification text-center panel-footer faint">
- {{$t('timeline.no_statuses')}}
+ <div
+ v-if="count===0"
+ class="new-status-notification text-center panel-footer faint"
+ >
+ {{ $t('timeline.no_statuses') }}
</div>
- <div v-else-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
- {{$t('timeline.no_more_statuses')}}
+ <div
+ v-else-if="bottomedOut"
+ class="new-status-notification text-center panel-footer faint"
+ >
+ {{ $t('timeline.no_more_statuses') }}
</div>
- <a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>
- <div class="new-status-notification text-center panel-footer">{{$t('timeline.load_older')}}</div>
+ <a
+ v-else-if="!timeline.loading"
+ href="#"
+ @click.prevent="fetchOlderStatuses()"
+ >
+ <div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div>
</a>
- <div v-else class="new-status-notification text-center panel-footer">
- <i class="icon-spin3 animate-spin"/>
+ <div
+ v-else
+ class="new-status-notification text-center panel-footer"
+ >
+ <i class="icon-spin3 animate-spin" />
</div>
</div>
</div>
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index a42b9c71..4adf8211 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -16,7 +16,7 @@ const UserAvatar = {
},
computed: {
imgSrc () {
- return this.showPlaceholder ? '/images/avi.png' : this.src
+ return this.showPlaceholder ? '/images/avi.png' : this.user.profile_image_url_original
}
},
methods: {
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index e5466fdf..9ffb28d8 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -3,9 +3,9 @@
class="avatar"
:alt="user.screen_name"
:title="user.screen_name"
- :src="user.profile_image_url_original"
+ :src="imgSrc"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
- :imageLoadError="imageLoadError"
+ :image-load-error="imageLoadError"
/>
</template>
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 7c6ffa89..82d3b835 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,12 +1,13 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
+import ProgressButton from '../progress_button/progress_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.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'
export default {
- props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered' ],
+ props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ],
data () {
return {
followRequestInProgress: false,
@@ -23,15 +24,15 @@ export default {
computed: {
classes () {
return [{
- 'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
- 'user-card-rounded': this.rounded === true, // set border-radius for all sides
- 'user-card-bordered': this.bordered === true // set border for all sides
+ 'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
+ 'user-card-rounded': this.rounded === true, // set border-radius for all sides
+ 'user-card-bordered': this.bordered === true // set border for all sides
}]
},
style () {
const color = this.$store.state.config.customTheme.colors
- ? this.$store.state.config.customTheme.colors.bg // v2
- : this.$store.state.config.colors.bg // v1
+ ? this.$store.state.config.customTheme.colors.bg // v2
+ : this.$store.state.config.colors.bg // v1
if (color) {
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
@@ -73,12 +74,12 @@ export default {
userHighlightType: {
get () {
const data = this.$store.state.config.highlight[this.user.screen_name]
- return data && data.type || 'disabled'
+ return (data && data.type) || 'disabled'
},
set (type) {
const data = this.$store.state.config.highlight[this.user.screen_name]
if (type !== 'disabled') {
- this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: data && data.color || '#FFFFFF', type })
+ this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type })
} else {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })
}
@@ -104,13 +105,14 @@ export default {
components: {
UserAvatar,
RemoteFollow,
- ModerationTools
+ ModerationTools,
+ ProgressButton
},
methods: {
followUser () {
const store = this.$store
this.followRequestInProgress = true
- requestFollow(this.user, store).then(({sent}) => {
+ requestFollow(this.user, store).then(({ sent }) => {
this.followRequestInProgress = false
this.followRequestSent = sent
})
@@ -135,13 +137,19 @@ export default {
unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id)
},
+ subscribeUser () {
+ return this.$store.dispatch('subscribeUser', this.user.id)
+ },
+ unsubscribeUser () {
+ return this.$store.dispatch('unsubscribeUser', this.user.id)
+ },
setProfileView (v) {
if (this.switcher) {
const store = this.$store
store.commit('setProfileView', { v })
}
},
- linkClicked ({target}) {
+ linkClicked ({ target }) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
@@ -154,6 +162,14 @@ export default {
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
+ },
+ zoomAvatar () {
+ const attachment = {
+ url: this.user.profile_image_url_original,
+ mimetype: 'image'
+ }
+ this.$store.dispatch('setMedia', [attachment])
+ this.$store.dispatch('setCurrent', attachment)
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index b4495673..fc18e240 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,65 +1,143 @@
<template>
-<div class="user-card" :class="classes" :style="style">
- <div class="panel-heading">
- <div class='user-info'>
- <div class='container'>
- <router-link :to="userProfileLink(user)">
- <UserAvatar :betterShadow="betterShadow" :user="user"/>
- </router-link>
- <div class="user-summary">
- <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-wrench usersettings" :title="$t('tool_tip.user_settings')"></i>
- </router-link>
- <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
- <i class="icon-link-ext usersettings"></i>
- </a>
+ <div
+ class="user-card"
+ :class="classes"
+ :style="style"
+ >
+ <div class="panel-heading">
+ <div class="user-info">
+ <div class="container">
+ <a
+ v-if="allowZoomingAvatar"
+ class="user-info-avatar-link"
+ @click="zoomAvatar"
+ >
+ <UserAvatar
+ :better-shadow="betterShadow"
+ :user="user"
+ />
+ <div class="user-info-avatar-link-overlay">
+ <i class="button-icon icon-zoom-in" />
+ </div>
+ </a>
+ <router-link
+ v-else
+ :to="userProfileLink(user)"
+ >
+ <UserAvatar
+ :better-shadow="betterShadow"
+ :user="user"
+ />
+ </router-link>
+ <div class="user-summary">
+ <div class="top-line">
+ <!-- eslint-disable vue/no-v-html -->
+ <div
+ v-if="user.name_html"
+ :title="user.name"
+ class="user-name"
+ v-html="user.name_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <div
+ v-else
+ :title="user.name"
+ class="user-name"
+ >
+ {{ user.name }}
+ </div>
+ <router-link
+ v-if="!isOtherUser"
+ :to="{ name: 'user-settings' }"
+ >
+ <i
+ class="button-icon icon-wrench usersettings"
+ :title="$t('tool_tip.user_settings')"
+ />
+ </router-link>
+ <a
+ v-if="isOtherUser && !user.is_local"
+ :href="user.statusnet_profile_url"
+ target="_blank"
+ >
+ <i class="icon-link-ext usersettings" />
+ </a>
+ </div>
+
+ <div class="bottom-line">
+ <router-link
+ class="user-screen-name"
+ :to="userProfileLink(user)"
+ >
+ @{{ user.screen_name }}
+ </router-link>
+ <span
+ v-if="!hideBio && !!visibleRole"
+ class="alert staff"
+ >{{ visibleRole }}</span>
+ <span v-if="user.locked"><i class="icon icon-lock" /></span>
+ <span
+ v-if="!hideUserStatsLocal && !hideBio"
+ class="dailyAvg"
+ >{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
+ </div>
</div>
-
- <div class="bottom-line">
- <router-link class="user-screen-name" :to="userProfileLink(user)">@{{user.screen_name}}</router-link>
- <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</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>
- </div>
- </div>
- </div>
- <div class="user-meta">
- <div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
- {{ $t('user_card.follows_you') }}
</div>
- <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"/>
- <label for="style-switcher" class='userHighlightSel select'>
- <select class="userHighlightSel" :id="'userHighlightSel'+user.id" v-model="userHighlightType">
- <option value="disabled">No highlight</option>
- <option value="solid">Solid bg</option>
- <option value="striped">Striped bg</option>
- <option value="side">Side stripe</option>
- </select>
- <i class="icon-down-open"/>
- </label>
+ <div class="user-meta">
+ <div
+ v-if="user.follows_you && loggedIn && isOtherUser"
+ class="following"
+ >
+ {{ $t('user_card.follows_you') }}
+ </div>
+ <div
+ v-if="isOtherUser && (loggedIn || !switcher)"
+ class="highlighter"
+ >
+ <!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
+ <input
+ v-if="userHighlightType !== 'disabled'"
+ :id="'userHighlightColorTx'+user.id"
+ v-model="userHighlightColor"
+ class="userHighlightText"
+ type="text"
+ >
+ <input
+ v-if="userHighlightType !== 'disabled'"
+ :id="'userHighlightColor'+user.id"
+ v-model="userHighlightColor"
+ class="userHighlightCl"
+ type="color"
+ >
+ <label
+ for="style-switcher"
+ class="userHighlightSel select"
+ >
+ <select
+ :id="'userHighlightSel'+user.id"
+ v-model="userHighlightType"
+ class="userHighlightSel"
+ >
+ <option value="disabled">No highlight</option>
+ <option value="solid">Solid bg</option>
+ <option value="striped">Striped bg</option>
+ <option value="side">Side stripe</option>
+ </select>
+ <i class="icon-down-open" />
+ </label>
+ </div>
</div>
- </div>
- <div v-if="isOtherUser" class="user-interactions">
- <div class="follow" v-if="loggedIn">
- <span v-if="user.following">
- <!--Following them!-->
- <button @click="unfollowUser" class="pressed" :disabled="followRequestInProgress" :title="$t('user_card.follow_unfollow')">
- <template v-if="followRequestInProgress">
- {{ $t('user_card.follow_progress') }}
- </template>
- <template v-else>
- {{ $t('user_card.following') }}
- </template>
- </button>
- </span>
- <span v-if="!user.following">
- <button @click="followUser" :disabled="followRequestInProgress" :title="followRequestSent ? $t('user_card.follow_again') : ''">
+ <div
+ v-if="loggedIn && isOtherUser"
+ class="user-interactions"
+ >
+ <div v-if="!user.following">
+ <button
+ class="btn btn-default btn-block"
+ :disabled="followRequestInProgress"
+ :title="followRequestSent ? $t('user_card.follow_again') : ''"
+ @click="followUser"
+ >
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
@@ -70,65 +148,148 @@
{{ $t('user_card.follow') }}
</template>
</button>
- </span>
- </div>
- <div class='mute' v-if='isOtherUser && loggedIn'>
- <span v-if='user.muted'>
- <button @click="unmuteUser" class="pressed">
+ </div>
+ <div v-else-if="followRequestInProgress">
+ <button
+ class="btn btn-default btn-block pressed"
+ disabled
+ :title="$t('user_card.follow_unfollow')"
+ @click="unfollowUser"
+ >
+ {{ $t('user_card.follow_progress') }}
+ </button>
+ </div>
+ <div
+ v-else
+ class="btn-group"
+ >
+ <button
+ class="btn btn-default pressed"
+ :title="$t('user_card.follow_unfollow')"
+ @click="unfollowUser"
+ >
+ {{ $t('user_card.following') }}
+ </button>
+ <ProgressButton
+ v-if="!user.subscribed"
+ class="btn btn-default"
+ :click="subscribeUser"
+ :title="$t('user_card.subscribe')"
+ >
+ <i class="icon-bell-alt" />
+ </ProgressButton>
+ <ProgressButton
+ v-else
+ class="btn btn-default pressed"
+ :click="unsubscribeUser"
+ :title="$t('user_card.unsubscribe')"
+ >
+ <i class="icon-bell-ringing-o" />
+ </ProgressButton>
+ </div>
+
+ <div>
+ <button
+ v-if="user.muted"
+ class="btn btn-default btn-block pressed"
+ @click="unmuteUser"
+ >
{{ $t('user_card.muted') }}
</button>
- </span>
- <span v-if='!user.muted'>
- <button @click="muteUser">
+ <button
+ v-else
+ class="btn btn-default btn-block"
+ @click="muteUser"
+ >
{{ $t('user_card.mute') }}
</button>
- </span>
- </div>
- <div v-if='!loggedIn && user.is_local'>
- <RemoteFollow :user="user" />
- </div>
- <div class='block' v-if='isOtherUser && loggedIn'>
- <span v-if='user.statusnet_blocking'>
- <button @click="unblockUser" class="pressed">
+ </div>
+
+ <div>
+ <button
+ v-if="user.statusnet_blocking"
+ class="btn btn-default btn-block pressed"
+ @click="unblockUser"
+ >
{{ $t('user_card.blocked') }}
</button>
- </span>
- <span v-if='!user.statusnet_blocking'>
- <button @click="blockUser">
+ <button
+ v-else
+ class="btn btn-default btn-block"
+ @click="blockUser"
+ >
{{ $t('user_card.block') }}
</button>
- </span>
- </div>
- <div class='block' v-if='isOtherUser && loggedIn'>
- <span>
- <button @click="reportUser">
+ </div>
+
+ <div>
+ <button
+ class="btn btn-default btn-block"
+ @click="reportUser"
+ >
{{ $t('user_card.report') }}
</button>
- </span>
+ </div>
+
+ <ModerationTools
+ v-if="loggedIn.role === &quot;admin&quot;"
+ :user="user"
+ />
+ </div>
+ <div
+ v-if="!loggedIn && user.is_local"
+ class="user-interactions"
+ >
+ <RemoteFollow :user="user" />
</div>
- <ModerationTools :user='user' v-if='loggedIn.role === "admin"'/>
</div>
</div>
- </div>
- <div class="panel-body" v-if="!hideBio">
- <div v-if="!hideUserStatsLocal && switcher" class="user-counts">
- <div class="user-count" v-on:click.prevent="setProfileView('statuses')">
- <h5>{{ $t('user_card.statuses') }}</h5>
- <span>{{user.statuses_count}} <br></span>
- </div>
- <div class="user-count" v-on:click.prevent="setProfileView('friends')">
- <h5>{{ $t('user_card.followees') }}</h5>
- <span>{{user.friends_count}}</span>
- </div>
- <div class="user-count" v-on:click.prevent="setProfileView('followers')">
- <h5>{{ $t('user_card.followers') }}</h5>
- <span>{{user.followers_count}}</span>
+ <div
+ v-if="!hideBio"
+ class="panel-body"
+ >
+ <div
+ v-if="!hideUserStatsLocal && switcher"
+ class="user-counts"
+ >
+ <div
+ class="user-count"
+ @click.prevent="setProfileView('statuses')"
+ >
+ <h5>{{ $t('user_card.statuses') }}</h5>
+ <span>{{ user.statuses_count }} <br></span>
+ </div>
+ <div
+ class="user-count"
+ @click.prevent="setProfileView('friends')"
+ >
+ <h5>{{ $t('user_card.followees') }}</h5>
+ <span>{{ user.friends_count }}</span>
+ </div>
+ <div
+ class="user-count"
+ @click.prevent="setProfileView('followers')"
+ >
+ <h5>{{ $t('user_card.followers') }}</h5>
+ <span>{{ user.followers_count }}</span>
+ </div>
</div>
+ <!-- eslint-disable vue/no-v-html -->
+ <p
+ v-if="!hideBio && user.description_html"
+ class="user-card-bio"
+ @click.prevent="linkClicked"
+ v-html="user.description_html"
+ />
+ <!-- eslint-enable vue/no-v-html -->
+ <p
+ v-else-if="!hideBio"
+ class="user-card-bio"
+ >
+ {{ user.description }}
+ </p>
</div>
- <p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="user-card-bio" v-html="user.description_html"></p>
- <p v-else-if="!hideBio" class="user-card-bio">{{ user.description }}</p>
</div>
-</div>
</template>
<script src="./user_card.js"></script>
@@ -138,7 +299,6 @@
.user-card {
background-size: cover;
- overflow: hidden;
.panel-heading {
padding: .5em 0;
@@ -153,6 +313,8 @@
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
}
p {
@@ -205,6 +367,7 @@
.container {
padding: 16px 0 6px;
display: flex;
+ align-items: flex-start;
max-height: 56px;
.avatar {
@@ -226,6 +389,35 @@
}
}
+ &-avatar-link {
+ position: relative;
+ cursor: pointer;
+
+ &-overlay {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.3);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ opacity: 0;
+ transition: opacity .2s ease;
+
+ i {
+ color: #FFF;
+ }
+ }
+
+ &:hover &-overlay {
+ opacity: 1;
+ }
+ }
+
.usersettings {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
@@ -358,43 +550,26 @@
}
}
.user-interactions {
+ position: relative;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
-
margin-right: -.75em;
- div {
+ > * {
flex: 1 0 0;
- margin-right: .75em;
- margin-bottom: .6em;
+ margin: 0 .75em .6em 0;
white-space: nowrap;
}
- .mute {
- max-width: 220px;
- min-height: 28px;
- }
-
- .follow {
- max-width: 220px;
- min-height: 28px;
- }
-
button {
- width: 100%;
- height: 100%;
margin: 0;
- }
- .remote-button {
- height: 28px !important;
- width: 92%;
- }
-
- .pressed {
- border-bottom-color: rgba(255, 255, 255, 0.2);
- border-top-color: rgba(0, 0, 0, 0.2);
+ &.pressed {
+ // TODO: This should be themed.
+ border-bottom-color: rgba(255, 255, 255, 0.2);
+ border-top-color: rgba(0, 0, 0, 0.2);
+ }
}
}
}
diff --git a/src/components/user_finder/user_finder.js b/src/components/user_finder/user_finder.js
deleted file mode 100644
index 27153f45..00000000
--- a/src/components/user_finder/user_finder.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const UserFinder = {
- data: () => ({
- username: undefined,
- hidden: true,
- error: false,
- loading: false
- }),
- methods: {
- findUser (username) {
- this.$router.push({ name: 'user-search', query: { query: username } })
- this.$refs.userSearchInput.focus()
- },
- toggleHidden () {
- this.hidden = !this.hidden
- this.$emit('toggled', this.hidden)
- }
- }
-}
-
-export default UserFinder
diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue
deleted file mode 100644
index a118ffe2..00000000
--- a/src/components/user_finder/user_finder.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
- <div>
- <div class="user-finder-container">
- <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
- <a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
- <template v-else>
- <input class="user-finder-input" ref="userSearchInput" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
- <button class="btn search-button" @click="findUser(username)">
- <i class="icon-search"/>
- </button>
- <i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
- </template>
- </div>
- </div>
-</template>
-
-<script src="./user_finder.js"></script>
-
-<style lang="scss">
-@import '../../_variables.scss';
-
-.user-finder-container {
- max-width: 100%;
- display: inline-flex;
- align-items: baseline;
- vertical-align: baseline;
-
-
- .user-finder-input,
- .search-button {
- height: 29px;
- }
- .user-finder-input {
- // TODO: do this properly without a rough guesstimate of 2 icons + paddings
- max-width: calc(100% - 30px - 30px - 20px);
- }
-
- .search-button {
- margin-left: .5em;
- margin-right: .5em;
- }
-}
-
-</style>
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 37e28ca5..c92630e3 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -1,13 +1,23 @@
<template>
<div class="user-panel">
-
- <div v-if="signedIn" key="user-panel" class="panel panel-default signed-in">
- <UserCard :user="user" :hideBio="true" rounded="top"/>
+ <div
+ v-if="signedIn"
+ key="user-panel"
+ class="panel panel-default signed-in"
+ >
+ <UserCard
+ :user="user"
+ :hide-bio="true"
+ rounded="top"
+ />
<div class="panel-footer">
- <post-status-form v-if='user'></post-status-form>
+ <post-status-form v-if="user" />
</div>
</div>
- <auth-form v-else key="user-panel"/>
+ <auth-form
+ v-else
+ key="user-panel"
+ />
</div>
</template>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index eab330e7..00055707 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -3,7 +3,6 @@ import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
import Conversation from '../conversation/conversation.vue'
-import ModerationTools from '../moderation_tools/moderation_tools.vue'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
@@ -23,19 +22,23 @@ const FriendList = withLoadMore({
additionalPropNames: ['userId']
})(List)
+const defaultTabKey = 'statuses'
+
const UserProfile = {
data () {
return {
error: false,
- userId: null
+ userId: null,
+ tab: defaultTabKey
}
},
created () {
const routeParams = this.$route.params
this.load(routeParams.name || routeParams.id)
+ this.tab = get(this.$route, 'query.tab', defaultTabKey)
},
destroyed () {
- this.cleanUp()
+ this.stopFetching()
},
computed: {
timeline () {
@@ -66,17 +69,36 @@ const UserProfile = {
},
methods: {
load (userNameOrId) {
+ const startFetchingTimeline = (timeline, userId) => {
+ // Clear timeline only if load another user's profile
+ if (userId !== this.$store.state.statuses.timelines[timeline].userId) {
+ this.$store.commit('clearTimeline', { timeline })
+ }
+ this.$store.dispatch('startFetchingTimeline', { timeline, userId })
+ }
+
+ const loadById = (userId) => {
+ this.userId = userId
+ startFetchingTimeline('user', userId)
+ startFetchingTimeline('media', userId)
+ if (this.isUs) {
+ startFetchingTimeline('favorites', userId)
+ }
+ // Fetch all pinned statuses immediately
+ this.$store.dispatch('fetchPinnedStatuses', userId)
+ }
+
+ // Reset view
+ this.userId = null
+ this.error = false
+
// Check if user data is already loaded in store
const user = this.$store.getters.findUser(userNameOrId)
if (user) {
- this.userId = user.id
- this.fetchTimelines()
+ loadById(user.id)
} else {
this.$store.dispatch('fetchUser', userNameOrId)
- .then(({ id }) => {
- this.userId = id
- this.fetchTimelines()
- })
+ .then(({ id }) => loadById(id))
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known error
@@ -89,40 +111,33 @@ const UserProfile = {
})
}
},
- fetchTimelines () {
- const userId = this.userId
- this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
- this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
- if (this.isUs) {
- this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
- }
- // Fetch all pinned statuses immediately
- this.$store.dispatch('fetchPinnedStatuses', userId)
- },
- cleanUp () {
+ stopFetching () {
this.$store.dispatch('stopFetching', 'user')
this.$store.dispatch('stopFetching', 'favorites')
this.$store.dispatch('stopFetching', 'media')
- this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.commit('clearTimeline', { timeline: 'favorites' })
- this.$store.commit('clearTimeline', { timeline: 'media' })
+ },
+ switchUser (userNameOrId) {
+ this.stopFetching()
+ this.load(userNameOrId)
+ },
+ onTabSwitch (tab) {
+ this.tab = tab
+ this.$router.replace({ query: { tab } })
}
},
watch: {
'$route.params.id': function (newVal) {
if (newVal) {
- this.cleanUp()
- this.load(newVal)
+ this.switchUser(newVal)
}
},
'$route.params.name': function (newVal) {
if (newVal) {
- this.cleanUp()
- this.load(newVal)
+ this.switchUser(newVal)
}
},
- $route () {
- this.$refs.tabSwitcher.activateTab(0)()
+ '$route.query': function (newVal) {
+ this.tab = newVal.tab || defaultTabKey
}
},
components: {
@@ -130,7 +145,6 @@ const UserProfile = {
Timeline,
FollowerList,
FriendList,
- ModerationTools,
FollowCard,
Conversation
}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 48b774ea..42516916 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,75 +1,105 @@
<template>
-<div>
- <div v-if="user" class="user-profile panel panel-default">
- <UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
- <tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
- <div :label="$t('user_card.statuses')" :disabled="!user.statuses_count">
- <div class="timeline">
- <template v-for="statusId in user.pinnedStatuseIds">
- <Conversation
- v-if="timeline.statusesObject[statusId]"
- class="status-fadein"
- :key="statusId"
- :statusoid="timeline.statusesObject[statusId]"
- :collapsable="true"
- :showPinned="true"
- />
- </template>
- </div>
+ <div>
+ <div
+ v-if="user"
+ class="user-profile panel panel-default"
+ >
+ <UserCard
+ :user="user"
+ :switcher="true"
+ :selected="timeline.viewing"
+ :allow-zooming-avatar="true"
+ rounded="top"
+ />
+ <tab-switcher
+ :active-tab="tab"
+ :render-only-focused="true"
+ :on-switch="onTabSwitch"
+ >
<Timeline
+ key="statuses"
+ :label="$t('user_card.statuses')"
:count="user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
- :timeline-name="'user'"
+ timeline-name="user"
:user-id="userId"
+ :pinned-status-ids="user.pinnedStatusIds"
/>
+ <div
+ v-if="followsTabVisible"
+ key="followees"
+ :label="$t('user_card.followees')"
+ :disabled="!user.friends_count"
+ >
+ <FriendList :user-id="userId">
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <FollowCard :user="item" />
+ </template>
+ </FriendList>
+ </div>
+ <div
+ v-if="followersTabVisible"
+ key="followers"
+ :label="$t('user_card.followers')"
+ :disabled="!user.followers_count"
+ >
+ <FollowerList :user-id="userId">
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <FollowCard
+ :user="item"
+ :no-follows-you="isUs"
+ />
+ </template>
+ </FollowerList>
+ </div>
+ <Timeline
+ key="media"
+ :label="$t('user_card.media')"
+ :disabled="!media.visibleStatuses.length"
+ :embedded="true"
+ :title="$t('user_card.media')"
+ timeline-name="media"
+ :timeline="media"
+ :user-id="userId"
+ />
+ <Timeline
+ v-if="isUs"
+ key="favorites"
+ :label="$t('user_card.favorites')"
+ :disabled="!favorites.visibleStatuses.length"
+ :embedded="true"
+ :title="$t('user_card.favorites')"
+ timeline-name="favorites"
+ :timeline="favorites"
+ />
+ </tab-switcher>
+ </div>
+ <div
+ v-else
+ class="panel user-profile-placeholder"
+ >
+ <div class="panel-heading">
+ <div class="title">
+ {{ $t('settings.profile_tab') }}
+ </div>
</div>
- <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
- <FriendList :userId="userId">
- <template slot="item" slot-scope="{item}">
- <FollowCard :user="item" />
- </template>
- </FriendList>
- </div>
- <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
- <FollowerList :userId="userId">
- <template slot="item" slot-scope="{item}">
- <FollowCard :user="item" :noFollowsYou="isUs" />
- </template>
- </FollowerList>
- </div>
- <Timeline
- :label="$t('user_card.media')"
- :disabled="!media.visibleStatuses.length"
- :embedded="true" :title="$t('user_card.media')"
- timeline-name="media"
- :timeline="media"
- :user-id="userId"
- />
- <Timeline
- v-if="isUs"
- :label="$t('user_card.favorites')"
- :disabled="!favorites.visibleStatuses.length"
- :embedded="true"
- :title="$t('user_card.favorites')"
- timeline-name="favorites"
- :timeline="favorites"
- />
- </tab-switcher>
- </div>
- <div v-else class="panel user-profile-placeholder">
- <div class="panel-heading">
- <div class="title">
- {{ $t('settings.profile_tab') }}
+ <div class="panel-body">
+ <span v-if="error">{{ error }}</span>
+ <i
+ v-else
+ class="icon-spin3 animate-spin"
+ />
</div>
</div>
- <div class="panel-body">
- <span v-if="error">{{ error }}</span>
- <i class="icon-spin3 animate-spin" v-else></i>
- </div>
</div>
-</div>
</template>
<script src="./user_profile.js"></script>
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index 432dd14d..c79a3707 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -1,45 +1,75 @@
<template>
-<div class="modal-view" @click="closeModal" v-if="isOpen">
- <div class="user-reporting-panel panel" @click.stop="">
- <div class="panel-heading">
- <div class="title">{{$t('user_reporting.title', [user.screen_name])}}</div>
- </div>
- <div class="panel-body">
- <div class="user-reporting-panel-left">
- <div>
- <p>{{$t('user_reporting.add_comment_description')}}</p>
- <textarea
- v-model="comment"
- class="form-control"
- :placeholder="$t('user_reporting.additional_comments')"
- rows="1"
- @input="resize"
- />
- </div>
- <div v-if="!user.is_local">
- <p>{{$t('user_reporting.forward_description')}}</p>
- <Checkbox v-model="forward">{{$t('user_reporting.forward_to', [remoteInstance])}}</Checkbox>
- </div>
- <div>
- <button class="btn btn-default" @click="reportUser" :disabled="processing">{{$t('user_reporting.submit')}}</button>
- <div class="alert error" v-if="error">
- {{$t('user_reporting.generic_error')}}
- </div>
+ <div
+ v-if="isOpen"
+ class="modal-view"
+ @click="closeModal"
+ >
+ <div
+ class="user-reporting-panel panel"
+ @click.stop=""
+ >
+ <div class="panel-heading">
+ <div class="title">
+ {{ $t('user_reporting.title', [user.screen_name]) }}
</div>
</div>
- <div class="user-reporting-panel-right">
- <List :items="statuses">
- <template slot="item" slot-scope="{item}">
- <div class="status-fadein user-reporting-panel-sitem">
- <Status :inConversation="false" :focused="false" :statusoid="item" />
- <Checkbox :checked="isChecked(item.id)" @change="checked => toggleStatus(checked, item.id)" />
+ <div class="panel-body">
+ <div class="user-reporting-panel-left">
+ <div>
+ <p>{{ $t('user_reporting.add_comment_description') }}</p>
+ <textarea
+ v-model="comment"
+ class="form-control"
+ :placeholder="$t('user_reporting.additional_comments')"
+ rows="1"
+ @input="resize"
+ />
+ </div>
+ <div v-if="!user.is_local">
+ <p>{{ $t('user_reporting.forward_description') }}</p>
+ <Checkbox v-model="forward">
+ {{ $t('user_reporting.forward_to', [remoteInstance]) }}
+ </Checkbox>
+ </div>
+ <div>
+ <button
+ class="btn btn-default"
+ :disabled="processing"
+ @click="reportUser"
+ >
+ {{ $t('user_reporting.submit') }}
+ </button>
+ <div
+ v-if="error"
+ class="alert error"
+ >
+ {{ $t('user_reporting.generic_error') }}
</div>
- </template>
- </List>
+ </div>
+ </div>
+ <div class="user-reporting-panel-right">
+ <List :items="statuses">
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <div class="status-fadein user-reporting-panel-sitem">
+ <Status
+ :in-conversation="false"
+ :focused="false"
+ :statusoid="item"
+ />
+ <Checkbox
+ :checked="isChecked(item.id)"
+ @change="checked => toggleStatus(checked, item.id)"
+ />
+ </div>
+ </template>
+ </List>
+ </div>
</div>
</div>
</div>
-</div>
</template>
<script src="./user_reporting_modal.js"></script>
diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js
deleted file mode 100644
index 62dafdf1..00000000
--- a/src/components/user_search/user_search.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import FollowCard from '../follow_card/follow_card.vue'
-import map from 'lodash/map'
-
-const userSearch = {
- components: {
- FollowCard
- },
- props: [
- 'query'
- ],
- data () {
- return {
- username: '',
- userIds: [],
- loading: false
- }
- },
- computed: {
- users () {
- return this.userIds.map(userId => this.$store.getters.findUser(userId))
- }
- },
- mounted () {
- this.search(this.query)
- },
- watch: {
- query (newV) {
- this.search(newV)
- }
- },
- methods: {
- newQuery (query) {
- this.$router.push({ name: 'user-search', query: { query } })
- this.$refs.userSearchInput.focus()
- },
- search (query) {
- if (!query) {
- this.users = []
- return
- }
- this.loading = true
- this.$store.dispatch('searchUsers', query)
- .then((res) => {
- this.loading = false
- this.userIds = map(res, 'id')
- })
- }
- }
-}
-
-export default userSearch
diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue
deleted file mode 100644
index 890b3c13..00000000
--- a/src/components/user_search/user_search.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<template>
- <div class="user-search panel panel-default">
- <div class="panel-heading">
- {{$t('nav.user_search')}}
- </div>
- <div class="user-search-input-container">
- <input class="user-finder-input" ref="userSearchInput" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
- <button class="btn search-button" @click="newQuery(username)">
- <i class="icon-search"/>
- </button>
- </div>
- <div v-if="loading" class="text-center loading-icon">
- <i class="icon-spin3 animate-spin"/>
- </div>
- <div v-else class="panel-body">
- <FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
- </div>
- </div>
-</template>
-
-<script src="./user_search.js"></script>
-
-<style lang="scss">
-.user-search-input-container {
- margin: 0.5em;
- display: flex;
- justify-content: center;
-
- .search-button {
- margin-left: 0.5em;
- }
-}
-
-.loading-icon {
- padding: 1em;
-}
-</style>
diff --git a/src/components/user_settings/confirm.vue b/src/components/user_settings/confirm.vue
index 46a42e38..69b3811b 100644
--- a/src/components/user_settings/confirm.vue
+++ b/src/components/user_settings/confirm.vue
@@ -1,13 +1,21 @@
<template>
-<div>
- <slot></slot>
- <button class="btn btn-default" @click="confirm" :disabled="disabled">
- {{$t('general.confirm')}}
- </button>
- <button class="btn btn-default" @click="cancel" :disabled="disabled">
- {{$t('general.cancel')}}
- </button>
-</div>
+ <div>
+ <slot />
+ <button
+ class="btn btn-default"
+ :disabled="disabled"
+ @click="confirm"
+ >
+ {{ $t('general.confirm') }}
+ </button>
+ <button
+ class="btn btn-default"
+ :disabled="disabled"
+ @click="cancel"
+ >
+ {{ $t('general.cancel') }}
+ </button>
+ </div>
</template>
<script src="./confirm.js">
diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js
index 2acee862..3090138a 100644
--- a/src/components/user_settings/mfa.js
+++ b/src/components/user_settings/mfa.js
@@ -7,6 +7,7 @@ import { mapState } from 'vuex'
const Mfa = {
data: () => ({
settings: { // current settings of MFA
+ available: false,
enabled: false,
totp: false
},
@@ -106,7 +107,7 @@ const Mfa = {
this.setupState.setupOTPState = 'confirm'
})
},
- doConfirmOTP () { // handler confirm enable OTP
+ doConfirmOTP () { // handler confirm enable OTP
this.error = null
this.backendInteractor.mfaConfirmOTP({
token: this.otpConfirmToken,
@@ -139,7 +140,9 @@ const Mfa = {
// fetch settings from server
async fetchSettings () {
let result = await this.backendInteractor.fetchSettingsMFA()
+ if (result.error) return
this.settings = result.settings
+ this.settings.available = true
return result
}
},
diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue
index ded426dd..14ea10a1 100644
--- a/src/components/user_settings/mfa.vue
+++ b/src/components/user_settings/mfa.vue
@@ -1,86 +1,138 @@
<template>
-<div class="setting-item mfa-settings" v-if="readyInit">
-
- <div class="mfa-heading">
- <h2>{{$t('settings.mfa.title')}}</h2>
- </div>
-
- <div>
- <div class="setting-item" v-if="!setupInProgress">
- <!-- Enabled methods -->
- <h3>{{$t('settings.mfa.authentication_methods')}}</h3>
- <totp-item :settings="settings" @deactivate="fetchSettings" @activate="activateOTP"/>
- <br />
-
- <div v-if="settings.enabled"> <!-- backup codes block-->
- <recovery-codes :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" />
- <button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes">
- {{$t('settings.mfa.generate_new_recovery_codes')}}
- </button>
+ <div
+ v-if="readyInit && settings.available"
+ class="setting-item mfa-settings"
+ >
+ <div class="mfa-heading">
+ <h2>{{ $t('settings.mfa.title') }}</h2>
+ </div>
- <div v-if="confirmNewBackupCodes">
- <confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes"
- :disabled="backupCodes.inProgress">
- <p class="warning">{{$t('settings.mfa.warning_of_generate_new_codes')}}</p>
- </confirm>
+ <div>
+ <div
+ v-if="!setupInProgress"
+ class="setting-item"
+ >
+ <!-- Enabled methods -->
+ <h3>{{ $t('settings.mfa.authentication_methods') }}</h3>
+ <totp-item
+ :settings="settings"
+ @deactivate="fetchSettings"
+ @activate="activateOTP"
+ />
+ <br>
+
+ <div v-if="settings.enabled">
+ <!-- backup codes block-->
+ <recovery-codes
+ v-if="!confirmNewBackupCodes"
+ :backup-codes="backupCodes"
+ />
+ <button
+ v-if="!confirmNewBackupCodes"
+ class="btn btn-default"
+ @click="getBackupCodes"
+ >
+ {{ $t('settings.mfa.generate_new_recovery_codes') }}
+ </button>
+
+ <div v-if="confirmNewBackupCodes">
+ <confirm
+ :disabled="backupCodes.inProgress"
+ @confirm="confirmBackupCodes"
+ @cancel="cancelBackupCodes"
+ >
+ <p class="warning">
+ {{ $t('settings.mfa.warning_of_generate_new_codes') }}
+ </p>
+ </confirm>
+ </div>
</div>
</div>
- </div>
-
- <div v-if="setupInProgress"> <!-- setup block-->
- <h3>{{$t('settings.mfa.setup_otp')}}</h3>
+ <div v-if="setupInProgress">
+ <!-- setup block-->
- <recovery-codes :backup-codes="backupCodes" v-if="!setupOTPInProgress"/>
+ <h3>{{ $t('settings.mfa.setup_otp') }}</h3>
+ <recovery-codes
+ v-if="!setupOTPInProgress"
+ :backup-codes="backupCodes"
+ />
- <button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP">
- {{$t('general.cancel')}}
- </button>
-
- <button class="btn btn-default" v-if="canSetupOTP" @click="setupOTP">
- {{$t('settings.mfa.setup_otp')}}
- </button>
-
- <template v-if="setupOTPInProgress">
- <i v-if="prepareOTP">{{$t('settings.mfa.wait_pre_setup_otp')}}</i>
+ <button
+ v-if="canSetupOTP"
+ class="btn btn-default"
+ @click="cancelSetup"
+ >
+ {{ $t('general.cancel') }}
+ </button>
- <div v-if="confirmOTP">
- <div class="setup-otp">
- <div class="qr-code">
- <h4>{{$t('settings.mfa.scan.title')}}</h4>
- <p>{{$t('settings.mfa.scan.desc')}}</p>
- <qrcode :value="otpSettings.provisioning_uri" :options="{ width: 200 }"></qrcode>
- <p>
- {{$t('settings.mfa.scan.secret_code')}}:
- {{otpSettings.key}}
- </p>
- </div>
+ <button
+ v-if="canSetupOTP"
+ class="btn btn-default"
+ @click="setupOTP"
+ >
+ {{ $t('settings.mfa.setup_otp') }}
+ </button>
- <div class="verify">
- <h4>{{$t('general.verify')}}</h4>
- <p>{{$t('settings.mfa.verify.desc')}}</p>
- <input type="text" v-model="otpConfirmToken">
+ <template v-if="setupOTPInProgress">
+ <i v-if="prepareOTP">{{ $t('settings.mfa.wait_pre_setup_otp') }}</i>
+
+ <div v-if="confirmOTP">
+ <div class="setup-otp">
+ <div class="qr-code">
+ <h4>{{ $t('settings.mfa.scan.title') }}</h4>
+ <p>{{ $t('settings.mfa.scan.desc') }}</p>
+ <qrcode
+ :value="otpSettings.provisioning_uri"
+ :options="{ width: 200 }"
+ />
+ <p>
+ {{ $t('settings.mfa.scan.secret_code') }}:
+ {{ otpSettings.key }}
+ </p>
+ </div>
- <p>{{$t('settings.enter_current_password_to_confirm')}}:</p>
- <input type="password" v-model="currentPassword">
- <div class="confirm-otp-actions">
- <button class="btn btn-default" @click="doConfirmOTP">
- {{$t('settings.mfa.confirm_and_enable')}}
- </button>
- <button class="btn btn-default" @click="cancelSetup">
- {{$t('general.cancel')}}
- </button>
+ <div class="verify">
+ <h4>{{ $t('general.verify') }}</h4>
+ <p>{{ $t('settings.mfa.verify.desc') }}</p>
+ <input
+ v-model="otpConfirmToken"
+ type="text"
+ >
+
+ <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p>
+ <input
+ v-model="currentPassword"
+ type="password"
+ >
+ <div class="confirm-otp-actions">
+ <button
+ class="btn btn-default"
+ @click="doConfirmOTP"
+ >
+ {{ $t('settings.mfa.confirm_and_enable') }}
+ </button>
+ <button
+ class="btn btn-default"
+ @click="cancelSetup"
+ >
+ {{ $t('general.cancel') }}
+ </button>
+ </div>
+ <div
+ v-if="error"
+ class="alert error"
+ >
+ {{ error }}
+ </div>
</div>
- <div class="alert error" v-if="error">{{error}}</div>
</div>
</div>
- </div>
- </template>
+ </template>
+ </div>
</div>
-
</div>
-</div>
</template>
<script src="./mfa.js"></script>
diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue
index c275bd63..e6c8ede2 100644
--- a/src/components/user_settings/mfa_backup_codes.vue
+++ b/src/components/user_settings/mfa_backup_codes.vue
@@ -1,12 +1,23 @@
<template>
-<div>
- <h4 v-if="displayTitle">{{$t('settings.mfa.recovery_codes')}}</h4>
- <i v-if="inProgress">{{$t('settings.mfa.waiting_a_recovery_codes')}}</i>
- <template v-if="ready">
- <p class="alert warning">{{$t('settings.mfa.recovery_codes_warning')}}</p>
- <ul class="backup-codes"><li v-for="code in backupCodes.codes">{{code}}</li></ul>
- </template>
-</div>
+ <div>
+ <h4 v-if="displayTitle">
+ {{ $t('settings.mfa.recovery_codes') }}
+ </h4>
+ <i v-if="inProgress">{{ $t('settings.mfa.waiting_a_recovery_codes') }}</i>
+ <template v-if="ready">
+ <p class="alert warning">
+ {{ $t('settings.mfa.recovery_codes_warning') }}
+ </p>
+ <ul class="backup-codes">
+ <li
+ v-for="code in backupCodes.codes"
+ :key="code"
+ >
+ {{ code }}
+ </li>
+ </ul>
+ </template>
+ </div>
</template>
<script src="./mfa_backup_codes.js"></script>
<style lang="scss">
diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/user_settings/mfa_totp.vue
index 6b73c8f4..c6f2cc7b 100644
--- a/src/components/user_settings/mfa_totp.vue
+++ b/src/components/user_settings/mfa_totp.vue
@@ -1,23 +1,43 @@
<template>
-<div>
- <div class="method-item">
- <strong>{{$t('settings.mfa.otp')}}</strong>
- <button class="btn btn-default" v-if="!isActivated" @click="doActivate">
- {{$t('general.enable')}}
- </button>
+ <div>
+ <div class="method-item">
+ <strong>{{ $t('settings.mfa.otp') }}</strong>
+ <button
+ v-if="!isActivated"
+ class="btn btn-default"
+ @click="doActivate"
+ >
+ {{ $t('general.enable') }}
+ </button>
- <button class="btn btn-default" :disabled="deactivate" @click="doDeactivate"
- v-if="isActivated">
- {{$t('general.disable')}}
- </button>
- </div>
+ <button
+ v-if="isActivated"
+ class="btn btn-default"
+ :disabled="deactivate"
+ @click="doDeactivate"
+ >
+ {{ $t('general.disable') }}
+ </button>
+ </div>
- <confirm @confirm="confirmDeactivate" @cancel="cancelDeactivate"
- :disabled="inProgress" v-if="deactivate">
- {{$t('settings.enter_current_password_to_confirm')}}:
- <input type="password" v-model="currentPassword">
- </confirm>
- <div class="alert error" v-if="error">{{error}}</div>
-</div>
+ <confirm
+ v-if="deactivate"
+ :disabled="inProgress"
+ @confirm="confirmDeactivate"
+ @cancel="cancelDeactivate"
+ >
+ {{ $t('settings.enter_current_password_to_confirm') }}:
+ <input
+ v-model="currentPassword"
+ type="password"
+ >
+ </confirm>
+ <div
+ v-if="error"
+ class="alert error"
+ >
+ {{ error }}
+ </div>
+ </div>
</template>
<script src="./mfa_totp.js"></script>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 69505806..b5a7f0df 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -12,11 +12,11 @@ import MuteCard from '../mute_card/mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji-input/emoji-input.vue'
+import suggestor from '../emoji-input/suggestor.js'
import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
-import userSearchApi from '../../services/new_api/user_search.js'
import Mfa from './mfa.vue'
const BlockList = withSubscription({
@@ -46,7 +46,9 @@ const UserSettings = {
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
+ banner: null,
bannerPreview: null,
+ background: null,
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null,
@@ -83,6 +85,22 @@ const UserSettings = {
user () {
return this.$store.state.users.currentUser
},
+ emojiUserSuggestor () {
+ return suggestor({
+ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ],
+ users: this.$store.state.users.users,
+ updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
+ })
+ },
+ emojiSuggestor () {
+ return suggestor({ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ] })
+ },
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
},
@@ -126,10 +144,10 @@ const UserSettings = {
hide_followers: this.hideFollowers,
show_role: this.showRole
/* eslint-enable camelcase */
- }}).then((user) => {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
- })
+ } }).then((user) => {
+ this.$store.commit('addNewUsers', [user])
+ this.$store.commit('setCurrentUser', user)
+ })
},
updateNotificationSettings () {
this.$store.state.api.backendInteractor
@@ -144,12 +162,12 @@ const UserSettings = {
if (file.size > this.$store.state.instance[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
- this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', {filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit})
+ this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
return
}
// eslint-disable-next-line no-undef
const reader = new FileReader()
- reader.onload = ({target}) => {
+ reader.onload = ({ target }) => {
const img = target.result
this[slot + 'Preview'] = img
this[slot] = file
@@ -185,7 +203,7 @@ const UserSettings = {
if (!this.bannerPreview) { return }
this.bannerUploading = true
- this.$store.state.api.backendInteractor.updateBanner({banner: this.banner})
+ this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
.then((user) => {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
@@ -198,22 +216,12 @@ const UserSettings = {
},
submitBg () {
if (!this.backgroundPreview) { return }
- let img = this.backgroundPreview
- // eslint-disable-next-line no-undef
- let imginfo = new Image()
- let cropX, cropY, cropW, cropH
- imginfo.src = img
- cropX = 0
- cropY = 0
- cropW = imginfo.width
- cropH = imginfo.width
+ let background = this.background
this.backgroundUploading = true
- this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
+ this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
if (!data.error) {
- let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
- clone.background_image = data.url
- this.$store.commit('addNewUsers', [clone])
- this.$store.commit('setCurrentUser', clone)
+ this.$store.commit('addNewUsers', [data])
+ this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
} else {
this.backgroundUploadError = this.$t('upload.error.base') + data.error
@@ -261,11 +269,11 @@ const UserSettings = {
this.deletingAccount = true
},
deleteAccount () {
- this.$store.state.api.backendInteractor.deleteAccount({password: this.deleteAccountConfirmPasswordInput})
+ this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
.then((res) => {
if (res.status === 'success') {
this.$store.dispatch('logout')
- this.$router.push({name: 'root'})
+ this.$router.push({ name: 'root' })
} else {
this.deleteAccountError = res.error
}
@@ -314,11 +322,8 @@ const UserSettings = {
})
},
queryUserIds (query) {
- return userSearchApi.search({query, store: this.$store})
- .then((users) => {
- this.$store.dispatch('addNewUsers', users)
- return map(users, 'id')
- })
+ return this.$store.dispatch('searchUsers', query)
+ .then((users) => map(users, 'id'))
},
blockUsers (ids) {
return this.$store.dispatch('blockUsers', ids)
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index bbe41f11..34ea8569 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -2,15 +2,23 @@
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
- {{$t('settings.user_settings')}}
+ {{ $t('settings.user_settings') }}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
- <div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
+ <div
+ v-if="currentSaveStateNotice.error"
+ class="alert error"
+ @click.prevent
+ >
{{ $t('settings.saving_err') }}
</div>
- <div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
+ <div
+ v-if="!currentSaveStateNotice.error"
+ class="alert transparent"
+ @click.prevent
+ >
{{ $t('settings.saving_ok') }}
</div>
</template>
@@ -19,133 +27,267 @@
<div class="panel-body profile-edit">
<tab-switcher>
<div :label="$t('settings.profile_tab')">
- <div class="setting-item" >
- <h2>{{$t('settings.name_bio')}}</h2>
- <p>{{$t('settings.name')}}</p>
+ <div class="setting-item">
+ <h2>{{ $t('settings.name_bio') }}</h2>
+ <p>{{ $t('settings.name') }}</p>
<EmojiInput
- type="text"
v-model="newName"
- id="username"
- classname="name-changer"
- />
- <p>{{$t('settings.bio')}}</p>
+ :suggest="emojiSuggestor"
+ >
+ <input
+ id="username"
+ v-model="newName"
+ classname="name-changer"
+ >
+ </EmojiInput>
+ <p>{{ $t('settings.bio') }}</p>
<EmojiInput
- type="textarea"
v-model="newBio"
- classname="bio"
- />
+ :suggest="emojiUserSuggestor"
+ >
+ <textarea
+ v-model="newBio"
+ classname="bio"
+ />
+ </EmojiInput>
<p>
- <input type="checkbox" v-model="newLocked" id="account-locked">
- <label for="account-locked">{{$t('settings.lock_account_description')}}</label>
+ <input
+ id="account-locked"
+ v-model="newLocked"
+ type="checkbox"
+ >
+ <label for="account-locked">{{ $t('settings.lock_account_description') }}</label>
</p>
<div>
- <label for="default-vis">{{$t('settings.default_vis')}}</label>
- <div id="default-vis" class="visibility-tray">
+ <label for="default-vis">{{ $t('settings.default_vis') }}</label>
+ <div
+ id="default-vis"
+ class="visibility-tray"
+ >
<scope-selector
- :showAll="true"
- :userDefault="newDefaultScope"
- :initialScope="newDefaultScope"
- :onScopeChange="changeVis"/>
+ :show-all="true"
+ :user-default="newDefaultScope"
+ :initial-scope="newDefaultScope"
+ :on-scope-change="changeVis"
+ />
</div>
</div>
<p>
- <input type="checkbox" v-model="newNoRichText" id="account-no-rich-text">
- <label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
+ <input
+ id="account-no-rich-text"
+ v-model="newNoRichText"
+ type="checkbox"
+ >
+ <label for="account-no-rich-text">{{ $t('settings.no_rich_text_description') }}</label>
</p>
<p>
- <input type="checkbox" v-model="hideFollows" id="account-hide-follows">
- <label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
+ <input
+ id="account-hide-follows"
+ v-model="hideFollows"
+ type="checkbox"
+ >
+ <label for="account-hide-follows">{{ $t('settings.hide_follows_description') }}</label>
</p>
<p>
- <input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
- <label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
+ <input
+ id="account-hide-followers"
+ v-model="hideFollowers"
+ type="checkbox"
+ >
+ <label for="account-hide-followers">{{ $t('settings.hide_followers_description') }}</label>
</p>
<p>
- <input type="checkbox" v-model="showRole" id="account-show-role">
- <label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
- <label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
+ <input
+ id="account-show-role"
+ v-model="showRole"
+ type="checkbox"
+ >
+ <label
+ v-if="role === 'admin'"
+ for="account-show-role"
+ >{{ $t('settings.show_admin_badge') }}</label>
+ <label
+ v-if="role === 'moderator'"
+ for="account-show-role"
+ >{{ $t('settings.show_moderator_badge') }}</label>
</p>
- <button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
+ <button
+ :disabled="newName && newName.length === 0"
+ class="btn btn-default"
+ @click="updateProfile"
+ >
+ {{ $t('general.submit') }}
+ </button>
</div>
<div class="setting-item">
- <h2>{{$t('settings.avatar')}}</h2>
- <p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
- <p>{{$t('settings.current_avatar')}}</p>
- <img :src="user.profile_image_url_original" class="current-avatar" />
- <p>{{$t('settings.set_new_avatar')}}</p>
- <button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
- <image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
+ <h2>{{ $t('settings.avatar') }}</h2>
+ <p class="visibility-notice">
+ {{ $t('settings.avatar_size_instruction') }}
+ </p>
+ <p>{{ $t('settings.current_avatar') }}</p>
+ <img
+ :src="user.profile_image_url_original"
+ class="current-avatar"
+ >
+ <p>{{ $t('settings.set_new_avatar') }}</p>
+ <button
+ v-show="pickAvatarBtnVisible"
+ id="pick-avatar"
+ class="btn"
+ type="button"
+ >
+ {{ $t('settings.upload_a_photo') }}
+ </button>
+ <image-cropper
+ trigger="#pick-avatar"
+ :submit-handler="submitAvatar"
+ @open="pickAvatarBtnVisible=false"
+ @close="pickAvatarBtnVisible=true"
+ />
</div>
<div class="setting-item">
- <h2>{{$t('settings.profile_banner')}}</h2>
- <p>{{$t('settings.current_profile_banner')}}</p>
- <img :src="user.cover_photo" class="banner" />
- <p>{{$t('settings.set_new_profile_banner')}}</p>
- <img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview" />
+ <h2>{{ $t('settings.profile_banner') }}</h2>
+ <p>{{ $t('settings.current_profile_banner') }}</p>
+ <img
+ :src="user.cover_photo"
+ class="banner"
+ >
+ <p>{{ $t('settings.set_new_profile_banner') }}</p>
+ <img
+ v-if="bannerPreview"
+ class="banner"
+ :src="bannerPreview"
+ >
<div>
- <input type="file" @change="uploadFile('banner', $event)" />
+ <input
+ type="file"
+ @change="uploadFile('banner', $event)"
+ >
</div>
- <i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
- <button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
- <div class='alert error' v-if="bannerUploadError">
+ <i
+ v-if="bannerUploading"
+ class=" icon-spin4 animate-spin uploading"
+ />
+ <button
+ v-else-if="bannerPreview"
+ class="btn btn-default"
+ @click="submitBanner"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <div
+ v-if="bannerUploadError"
+ class="alert error"
+ >
Error: {{ bannerUploadError }}
- <i class="button-icon icon-cancel" @click="clearUploadError('banner')"></i>
+ <i
+ class="button-icon icon-cancel"
+ @click="clearUploadError('banner')"
+ />
</div>
</div>
<div class="setting-item">
- <h2>{{$t('settings.profile_background')}}</h2>
- <p>{{$t('settings.set_new_profile_background')}}</p>
- <img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview" />
+ <h2>{{ $t('settings.profile_background') }}</h2>
+ <p>{{ $t('settings.set_new_profile_background') }}</p>
+ <img
+ v-if="backgroundPreview"
+ class="bg"
+ :src="backgroundPreview"
+ >
<div>
- <input type="file" @change="uploadFile('background', $event)" />
+ <input
+ type="file"
+ @change="uploadFile('background', $event)"
+ >
</div>
- <i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
- <button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
- <div class='alert error' v-if="backgroundUploadError">
+ <i
+ v-if="backgroundUploading"
+ class=" icon-spin4 animate-spin uploading"
+ />
+ <button
+ v-else-if="backgroundPreview"
+ class="btn btn-default"
+ @click="submitBg"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <div
+ v-if="backgroundUploadError"
+ class="alert error"
+ >
Error: {{ backgroundUploadError }}
- <i class="button-icon icon-cancel" @click="clearUploadError('background')"></i>
+ <i
+ class="button-icon icon-cancel"
+ @click="clearUploadError('background')"
+ />
</div>
</div>
</div>
<div :label="$t('settings.security_tab')">
<div class="setting-item">
- <h2>{{$t('settings.change_password')}}</h2>
+ <h2>{{ $t('settings.change_password') }}</h2>
<div>
- <p>{{$t('settings.current_password')}}</p>
- <input type="password" v-model="changePasswordInputs[0]">
+ <p>{{ $t('settings.current_password') }}</p>
+ <input
+ v-model="changePasswordInputs[0]"
+ type="password"
+ >
</div>
<div>
- <p>{{$t('settings.new_password')}}</p>
- <input type="password" v-model="changePasswordInputs[1]">
+ <p>{{ $t('settings.new_password') }}</p>
+ <input
+ v-model="changePasswordInputs[1]"
+ type="password"
+ >
</div>
<div>
- <p>{{$t('settings.confirm_new_password')}}</p>
- <input type="password" v-model="changePasswordInputs[2]">
+ <p>{{ $t('settings.confirm_new_password') }}</p>
+ <input
+ v-model="changePasswordInputs[2]"
+ type="password"
+ >
</div>
- <button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
- <p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
- <p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
- <p v-if="changePasswordError">{{changePasswordError}}</p>
+ <button
+ class="btn btn-default"
+ @click="changePassword"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <p v-if="changedPassword">
+ {{ $t('settings.changed_password') }}
+ </p>
+ <p v-else-if="changePasswordError !== false">
+ {{ $t('settings.change_password_error') }}
+ </p>
+ <p v-if="changePasswordError">
+ {{ changePasswordError }}
+ </p>
</div>
<div class="setting-item">
- <h2>{{$t('settings.oauth_tokens')}}</h2>
+ <h2>{{ $t('settings.oauth_tokens') }}</h2>
<table class="oauth-tokens">
<thead>
<tr>
- <th>{{$t('settings.app_name')}}</th>
- <th>{{$t('settings.valid_until')}}</th>
- <th></th>
+ <th>{{ $t('settings.app_name') }}</th>
+ <th>{{ $t('settings.valid_until') }}</th>
+ <th />
</tr>
</thead>
<tbody>
- <tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
- <td>{{oauthToken.appName}}</td>
- <td>{{oauthToken.validUntil}}</td>
+ <tr
+ v-for="oauthToken in oauthTokens"
+ :key="oauthToken.id"
+ >
+ <td>{{ oauthToken.appName }}</td>
+ <td>{{ oauthToken.validUntil }}</td>
<td class="actions">
- <button class="btn btn-default" @click="revokeToken(oauthToken.id)">
- {{$t('settings.revoke_token')}}
+ <button
+ class="btn btn-default"
+ @click="revokeToken(oauthToken.id)"
+ >
+ {{ $t('settings.revoke_token') }}
</button>
</td>
</tr>
@@ -154,123 +296,250 @@
</div>
<mfa />
<div class="setting-item">
- <h2>{{$t('settings.delete_account')}}</h2>
- <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
+ <h2>{{ $t('settings.delete_account') }}</h2>
+ <p v-if="!deletingAccount">
+ {{ $t('settings.delete_account_description') }}
+ </p>
<div v-if="deletingAccount">
- <p>{{$t('settings.delete_account_instructions')}}</p>
- <p>{{$t('login.password')}}</p>
- <input type="password" v-model="deleteAccountConfirmPasswordInput">
- <button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
+ <p>{{ $t('settings.delete_account_instructions') }}</p>
+ <p>{{ $t('login.password') }}</p>
+ <input
+ v-model="deleteAccountConfirmPasswordInput"
+ type="password"
+ >
+ <button
+ class="btn btn-default"
+ @click="deleteAccount"
+ >
+ {{ $t('settings.delete_account') }}
+ </button>
</div>
- <p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
- <p v-if="deleteAccountError">{{deleteAccountError}}</p>
- <button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
+ <p v-if="deleteAccountError !== false">
+ {{ $t('settings.delete_account_error') }}
+ </p>
+ <p v-if="deleteAccountError">
+ {{ deleteAccountError }}
+ </p>
+ <button
+ v-if="!deletingAccount"
+ class="btn btn-default"
+ @click="confirmDelete"
+ >
+ {{ $t('general.submit') }}
+ </button>
</div>
</div>
- <div :label="$t('settings.notifications')" v-if="pleromaBackend">
+ <div
+ v-if="pleromaBackend"
+ :label="$t('settings.notifications')"
+ >
<div class="setting-item">
<div class="select-multiple">
- <span class="label">{{$t('settings.notification_setting')}}</span>
+ <span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list">
<li>
- <input type="checkbox" id="notification-setting-follows" v-model="notificationSettings.follows">
+ <input
+ id="notification-setting-follows"
+ v-model="notificationSettings.follows"
+ type="checkbox"
+ >
<label for="notification-setting-follows">
- {{$t('settings.notification_setting_follows')}}
+ {{ $t('settings.notification_setting_follows') }}
</label>
</li>
<li>
- <input type="checkbox" id="notification-setting-followers" v-model="notificationSettings.followers">
+ <input
+ id="notification-setting-followers"
+ v-model="notificationSettings.followers"
+ type="checkbox"
+ >
<label for="notification-setting-followers">
- {{$t('settings.notification_setting_followers')}}
+ {{ $t('settings.notification_setting_followers') }}
</label>
</li>
<li>
- <input type="checkbox" id="notification-setting-non-follows" v-model="notificationSettings.non_follows">
+ <input
+ id="notification-setting-non-follows"
+ v-model="notificationSettings.non_follows"
+ type="checkbox"
+ >
<label for="notification-setting-non-follows">
- {{$t('settings.notification_setting_non_follows')}}
+ {{ $t('settings.notification_setting_non_follows') }}
</label>
</li>
<li>
- <input type="checkbox" id="notification-setting-non-followers" v-model="notificationSettings.non_followers">
+ <input
+ id="notification-setting-non-followers"
+ v-model="notificationSettings.non_followers"
+ type="checkbox"
+ >
<label for="notification-setting-non-followers">
- {{$t('settings.notification_setting_non_followers')}}
+ {{ $t('settings.notification_setting_non_followers') }}
</label>
</li>
</ul>
</div>
- <p>{{$t('settings.notification_mutes')}}</p>
- <p>{{$t('settings.notification_blocks')}}</p>
- <button class="btn btn-default" @click="updateNotificationSettings">{{$t('general.submit')}}</button>
+ <p>{{ $t('settings.notification_mutes') }}</p>
+ <p>{{ $t('settings.notification_blocks') }}</p>
+ <button
+ class="btn btn-default"
+ @click="updateNotificationSettings"
+ >
+ {{ $t('general.submit') }}
+ </button>
</div>
</div>
- <div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend">
+ <div
+ v-if="pleromaBackend"
+ :label="$t('settings.data_import_export_tab')"
+ >
<div class="setting-item">
- <h2>{{$t('settings.follow_import')}}</h2>
- <p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
- <Importer :submitHandler="importFollows" :successMessage="$t('settings.follows_imported')" :errorMessage="$t('settings.follow_import_error')" />
+ <h2>{{ $t('settings.follow_import') }}</h2>
+ <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
+ <Importer
+ :submit-handler="importFollows"
+ :success-message="$t('settings.follows_imported')"
+ :error-message="$t('settings.follow_import_error')"
+ />
</div>
<div class="setting-item">
- <h2>{{$t('settings.follow_export')}}</h2>
- <Exporter :getContent="getFollowsContent" filename="friends.csv" :exportButtonLabel="$t('settings.follow_export_button')" />
+ <h2>{{ $t('settings.follow_export') }}</h2>
+ <Exporter
+ :get-content="getFollowsContent"
+ filename="friends.csv"
+ :export-button-label="$t('settings.follow_export_button')"
+ />
</div>
<div class="setting-item">
- <h2>{{$t('settings.block_import')}}</h2>
- <p>{{$t('settings.import_blocks_from_a_csv_file')}}</p>
- <Importer :submitHandler="importBlocks" :successMessage="$t('settings.blocks_imported')" :errorMessage="$t('settings.block_import_error')" />
+ <h2>{{ $t('settings.block_import') }}</h2>
+ <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
+ <Importer
+ :submit-handler="importBlocks"
+ :success-message="$t('settings.blocks_imported')"
+ :error-message="$t('settings.block_import_error')"
+ />
</div>
<div class="setting-item">
- <h2>{{$t('settings.block_export')}}</h2>
- <Exporter :getContent="getBlocksContent" filename="blocks.csv" :exportButtonLabel="$t('settings.block_export_button')" />
+ <h2>{{ $t('settings.block_export') }}</h2>
+ <Exporter
+ :get-content="getBlocksContent"
+ filename="blocks.csv"
+ :export-button-label="$t('settings.block_export_button')"
+ />
</div>
</div>
<div :label="$t('settings.blocks_tab')">
<div class="profile-edit-usersearch-wrapper">
- <Autosuggest :filter="filterUnblockedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')">
- <BlockCard slot-scope="row" :userId="row.item"/>
+ <Autosuggest
+ :filter="filterUnblockedUsers"
+ :query="queryUserIds"
+ :placeholder="$t('settings.search_user_to_block')"
+ >
+ <BlockCard
+ slot-scope="row"
+ :user-id="row.item"
+ />
</Autosuggest>
</div>
- <BlockList :refresh="true" :getKey="identity">
- <template slot="header" slot-scope="{selected}">
+ <BlockList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
<div class="profile-edit-bulk-actions">
- <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => blockUsers(selected)">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => blockUsers(selected)"
+ >
{{ $t('user_card.block') }}
- <template slot="progress">{{ $t('user_card.block_progress') }}</template>
+ <template slot="progress">
+ {{ $t('user_card.block_progress') }}
+ </template>
</ProgressButton>
- <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unblockUsers(selected)">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unblockUsers(selected)"
+ >
{{ $t('user_card.unblock') }}
- <template slot="progress">{{ $t('user_card.unblock_progress') }}</template>
+ <template slot="progress">
+ {{ $t('user_card.unblock_progress') }}
+ </template>
</ProgressButton>
</div>
</template>
- <template slot="item" slot-scope="{item}"><BlockCard :userId="item" /></template>
- <template slot="empty">{{$t('settings.no_blocks')}}</template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <BlockCard :user-id="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_blocks') }}
+ </template>
</BlockList>
</div>
<div :label="$t('settings.mutes_tab')">
<div class="profile-edit-usersearch-wrapper">
- <Autosuggest :filter="filterUnMutedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')">
- <MuteCard slot-scope="row" :userId="row.item"/>
+ <Autosuggest
+ :filter="filterUnMutedUsers"
+ :query="queryUserIds"
+ :placeholder="$t('settings.search_user_to_mute')"
+ >
+ <MuteCard
+ slot-scope="row"
+ :user-id="row.item"
+ />
</Autosuggest>
</div>
- <MuteList :refresh="true" :getKey="identity">
- <template slot="header" slot-scope="{selected}">
+ <MuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
<div class="profile-edit-bulk-actions">
- <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => muteUsers(selected)">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => muteUsers(selected)"
+ >
{{ $t('user_card.mute') }}
- <template slot="progress">{{ $t('user_card.mute_progress') }}</template>
+ <template slot="progress">
+ {{ $t('user_card.mute_progress') }}
+ </template>
</ProgressButton>
- <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unmuteUsers(selected)">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteUsers(selected)"
+ >
{{ $t('user_card.unmute') }}
- <template slot="progress">{{ $t('user_card.unmute_progress') }}</template>
+ <template slot="progress">
+ {{ $t('user_card.unmute_progress') }}
+ </template>
</ProgressButton>
</div>
</template>
- <template slot="item" slot-scope="{item}"><MuteCard :userId="item" /></template>
- <template slot="empty">{{$t('settings.no_mutes')}}</template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <MuteCard :user-id="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
</MuteList>
</div>
</tab-switcher>
diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue
index 68de201e..97ddf1cd 100644
--- a/src/components/video_attachment/video_attachment.vue
+++ b/src/components/video_attachment/video_attachment.vue
@@ -1,10 +1,11 @@
<template>
- <video class="video"
- @loadeddata="onVideoDataLoad"
+ <video
+ class="video"
:src="attachment.url"
:loop="loopVideo"
:controls="controls"
playsinline
+ @loadeddata="onVideoDataLoad"
/>
</template>
diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js
index 7ae602a2..8fab6c4d 100644
--- a/src/components/who_to_follow/who_to_follow.js
+++ b/src/components/who_to_follow/who_to_follow.js
@@ -21,7 +21,8 @@ const WhoToFollow = {
name: i.display_name,
screen_name: i.acct,
profile_image_url: i.avatar || '/images/avi.png',
- profile_image_url_original: i.avatar || '/images/avi.png'
+ profile_image_url_original: i.avatar || '/images/avi.png',
+ statusnet_profile_url: i.url
}
this.users.push(user)
@@ -37,7 +38,7 @@ const WhoToFollow = {
getWhoToFollow () {
const credentials = this.$store.state.users.currentUser.credentials
if (credentials) {
- apiService.suggestions({credentials: credentials})
+ apiService.suggestions({ credentials: credentials })
.then((reply) => {
this.showWhoToFollow(reply)
})
diff --git a/src/components/who_to_follow/who_to_follow.vue b/src/components/who_to_follow/who_to_follow.vue
index 8bc9a728..3a17d0e2 100644
--- a/src/components/who_to_follow/who_to_follow.vue
+++ b/src/components/who_to_follow/who_to_follow.vue
@@ -1,10 +1,15 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
- {{$t('who_to_follow.who_to_follow')}}
+ {{ $t('who_to_follow.who_to_follow') }}
</div>
<div class="panel-body">
- <FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
+ <FollowCard
+ v-for="user in users"
+ :key="user.id"
+ :user="user"
+ class="list-item"
+ />
</div>
</div>
</template>
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 a56a27ea..7d01678b 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
@@ -29,7 +29,7 @@ function getWhoToFollow (panel) {
panel.usersToFollow.forEach(toFollow => {
toFollow.name = 'Loading...'
})
- apiService.suggestions({credentials: credentials})
+ apiService.suggestions({ credentials: credentials })
.then((reply) => {
showWhoToFollow(panel, reply)
})
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 74e82789..518acd97 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
@@ -3,19 +3,23 @@
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
- {{$t('who_to_follow.who_to_follow')}}
+ {{ $t('who_to_follow.who_to_follow') }}
</div>
</div>
<div class="who-to-follow">
- <p v-for="user in usersToFollow" class="who-to-follow-items">
- <img v-bind:src="user.img" />
- <router-link v-bind:to="userProfileLink(user.id, user.name)">
- {{user.name}}
- </router-link><br />
+ <p
+ v-for="user in usersToFollow"
+ :key="user.id"
+ class="who-to-follow-items"
+ >
+ <img :src="user.img">
+ <router-link :to="userProfileLink(user.id, user.name)">
+ {{ user.name }}
+ </router-link><br>
</p>
<p class="who-to-follow-more">
<router-link :to="{ name: 'who-to-follow' }">
- {{$t('who_to_follow.more')}}
+ {{ $t('who_to_follow.more') }}
</router-link>
</p>
</div>