aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.scss35
-rw-r--r--src/components/attachment/attachment.js38
-rw-r--r--src/components/attachment/attachment.vue9
-rw-r--r--src/components/notification/notification.js11
-rw-r--r--src/components/notification/notification.vue5
-rw-r--r--src/components/notifications/notifications.scss4
-rw-r--r--src/components/post_status_form/post_status_form.js10
-rw-r--r--src/components/post_status_form/post_status_form.vue68
-rw-r--r--src/components/registration/registration.js10
-rw-r--r--src/components/registration/registration.vue4
-rw-r--r--src/components/retweet_button/retweet_button.js2
-rw-r--r--src/components/retweet_button/retweet_button.vue4
-rw-r--r--src/components/settings/settings.js26
-rw-r--r--src/components/settings/settings.vue122
-rw-r--r--src/components/status/status.js73
-rw-r--r--src/components/status/status.vue82
-rw-r--r--src/components/style_switcher/style_switcher.js91
-rw-r--r--src/components/style_switcher/style_switcher.vue15
-rw-r--r--src/components/timeline/timeline.js18
-rw-r--r--src/components/user_card/user_card.vue10
-rw-r--r--src/components/user_card_content/user_card_content.js28
-rw-r--r--src/components/user_card_content/user_card_content.vue128
-rw-r--r--src/components/user_settings/user_settings.js26
-rw-r--r--src/components/user_settings/user_settings.vue43
-rw-r--r--src/i18n/messages.js36
-rw-r--r--src/main.js13
-rw-r--r--src/modules/config.js21
-rw-r--r--src/modules/users.js4
-rw-r--r--src/services/api/api.service.js1
-rw-r--r--src/services/user_highlighter/user_highlighter.js48
30 files changed, 781 insertions, 204 deletions
diff --git a/src/App.scss b/src/App.scss
index f830a33b..3b8b3224 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -142,6 +142,14 @@ input, textarea, .select {
color: $fallback--fg;
color: var(--fg, $fallback--fg);
}
+ &:disabled,
+ {
+ &,
+ & + label,
+ & + label::before {
+ opacity: .5;
+ }
+ }
+ label::before {
display: inline-block;
content: '✔';
@@ -168,6 +176,13 @@ input, textarea, .select {
}
}
+option {
+ color: $fallback--fg;
+ color: var(--fg, $fallback--fg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+}
+
i[class*=icon-] {
color: $fallback--icon;
color: var(--icon, $fallback--icon)
@@ -426,3 +441,23 @@ nav {
text-align: right;
padding-right: 20px;
}
+
+.visibility-tray {
+ font-size: 1.2em;
+ padding: 3px;
+ cursor: pointer;
+
+ .selected {
+ color: $fallback--lightFg;
+ color: var(--lightFg, $fallback--lightFg);
+ }
+}
+
+.visibility-notice {
+ padding: .5em;
+ border: 1px solid $fallback--faint;
+ border: 1px solid var(--faint, $fallback--faint);
+ border-radius: $fallback--inputRadius;
+ border-radius: var(--inputRadius, $fallback--inputRadius);
+}
+
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index d9bc4477..cc19714d 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -13,9 +13,10 @@ const Attachment = {
return {
nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw,
+ loopVideo: this.$store.state.config.loopVideo,
showHidden: false,
loading: false,
- img: document.createElement('img')
+ img: this.type === 'image' && document.createElement('img')
}
},
components: {
@@ -45,14 +46,35 @@ const Attachment = {
}
},
toggleHidden () {
- if (this.img.onload) {
- this.img.onload()
+ if (this.img) {
+ if (this.img.onload) {
+ this.img.onload()
+ } else {
+ this.loading = true
+ this.img.src = this.attachment.url
+ this.img.onload = () => {
+ this.loading = false
+ this.showHidden = !this.showHidden
+ }
+ }
} else {
- this.loading = true
- this.img.src = this.attachment.url
- this.img.onload = () => {
- this.loading = false
- this.showHidden = !this.showHidden
+ this.showHidden = !this.showHidden
+ }
+ },
+ onVideoDataLoad (e) {
+ if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') {
+ // non-zero if video has audio track
+ if (e.srcElement.webkitAudioDecodedByteCount > 0) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ }
+ } else if (typeof e.srcElement.mozHasAudio !== 'undefined') {
+ // true if video has audio track
+ if (e.srcElement.mozHasAudio) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ }
+ } else if (typeof e.srcElement.audioTracks !== 'undefined') {
+ if (e.srcElement.audioTracks.length > 0) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
}
}
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index c48fb16b..d01c8566 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -2,7 +2,7 @@
<div v-if="size==='hide'">
<a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a>
</div>
- <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth}" v-show="!isEmpty">
+ <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty">
<a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
<img :key="nsfwImage" :src="nsfwImage"/>
</a>
@@ -14,7 +14,7 @@
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a>
- <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video>
+ <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video>
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@@ -38,7 +38,6 @@
.attachments {
display: flex;
flex-wrap: wrap;
- margin-right: -0.7em;
.attachment.media-upload-container {
flex: 0 0 auto;
@@ -50,6 +49,10 @@
margin-right: 0.5em;
}
+ .nsfw-placeholder {
+ cursor: pointer;
+ }
+
.small-attachment {
&.image, &.video {
max-width: 35%;
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 3a274374..c786f2cc 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import StillImage from '../still-image/still-image.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
+import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
const Notification = {
data () {
@@ -18,6 +19,16 @@ const Notification = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
}
+ },
+ computed: {
+ userClass () {
+ return highlightClass(this.notification.action.user)
+ },
+ userStyle () {
+ const highlight = this.$store.state.config.highlight
+ const user = this.notification.action.user
+ return highlightStyle(highlight[user.screen_name])
+ }
}
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index eed598a8..bb76ddf8 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,6 +1,6 @@
<template>
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
- <div class="non-mention" v-else>
+ <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage class='avatar-compact' :src="notification.action.user.profile_image_url_original"/>
</a>
@@ -10,7 +10,8 @@
</div>
<span class="notification-details">
<div class="name-and-action">
- <span class="username" :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+ <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
+ <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
<span v-if="notification.type === 'favorite'">
<i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small>
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 008530b4..5853c68e 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -45,8 +45,7 @@
}
.unseen {
- border-left: 4px solid $fallback--cRed;
- border-left: 4px solid var(--cRed, $fallback--cRed);
+ box-shadow: inset 4px 0 0 var(--cRed, $fallback--cRed);
padding-left: 0;
}
}
@@ -56,7 +55,6 @@
display: flex;
border-bottom: 1px solid;
border-bottom-color: inherit;
- padding-left: 4px;
.avatar-compact {
width: 32px;
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 4f4c6aca..ff3bb906 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -31,6 +31,10 @@ const PostStatusForm = {
},
mounted () {
this.resize(this.$refs.textarea)
+
+ if (this.replyTo) {
+ this.$refs.textarea.focus()
+ }
},
data () {
const preset = this.$route.query.message
@@ -50,7 +54,7 @@ const PostStatusForm = {
newStatus: {
status: statusText,
files: [],
- visibility: this.messageScope || 'public'
+ visibility: this.messageScope || this.$store.state.users.currentUser.default_scope
},
caret: 0
}
@@ -87,11 +91,11 @@ const PostStatusForm = {
return false
}
return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
- // eslint-disable-next-line camelcase
screen_name: `:${shortcode}:`,
name: '',
utf: utf || '',
- img: image_url,
+ // eslint-disable-next-line camelcase
+ img: utf ? '' : this.$store.state.config.server + image_url,
highlighted: index === this.highlighted
}))
} else {
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 7aa0e7c4..2b84758b 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -65,12 +65,14 @@
<i class="icon-cancel" @click="clearError"></i>
</div>
<div class="attachments">
- <div class="media-upload-container attachment" v-for="file in newStatus.files">
+ <div class="media-upload-wrapper" v-for="file in newStatus.files">
<i class="fa icon-cancel" @click="removeMediaFile(file)"></i>
- <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
- <video v-if="type(file) === 'video'" :src="file.image" controls></video>
- <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
- <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ <div class="media-upload-container attachment">
+ <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
+ <video v-if="type(file) === 'video'" :src="file.image" controls></video>
+ <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
+ <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ </div>
</div>
</div>
</form>
@@ -99,25 +101,6 @@
}
}
-.post-status-form .visibility-tray {
- font-size: 1.2em;
- padding: 3px;
- cursor: pointer;
-
- .selected {
- color: $fallback--lightFg;
- color: var(--lightFg, $fallback--lightFg);
- }
-}
-
-.visibility-notice {
- padding: .5em;
- border: 1px solid $fallback--faint;
- border: 1px solid var(--faint, $fallback--faint);
- border-radius: $fallback--inputRadius;
- border-radius: var(--inputRadius, $fallback--inputRadius);
-}
-
.post-status-form, .login {
.form-bottom {
display: flex;
@@ -139,14 +122,49 @@
text-align: center;
}
+ .media-upload-wrapper {
+ flex: 0 0 auto;
+ max-width: 100%;
+ min-width: 50px;
+ margin-right: .2em;
+ margin-bottom: .5em;
+
+ .icon-cancel {
+ display: inline-block;
+ position: static;
+ margin: 0;
+ padding-bottom: 0;
+ margin-left: $fallback--attachmentRadius;
+ margin-left: var(--attachmentRadius, $fallback--attachmentRadius);
+ background-color: $fallback--btn;
+ background-color: var(--btn, $fallback--btn);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+
.attachments {
padding: 0 0.5em;
.attachment {
+ margin: 0;
position: relative;
+ flex: 0 0 auto;
border: 1px solid $fallback--border;
border: 1px solid var(--border, $fallback--border);
- margin: 0.5em 0.8em 0.2em 0;
+ text-align: center;
+
+ audio {
+ min-width: 300px;
+ flex: 1 0 auto;
+ }
+
+ a {
+ display: block;
+ text-align: left;
+ line-height: 1.2;
+ padding: .5em;
+ }
}
i {
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 771b3b27..73840608 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -5,17 +5,23 @@ const registration = {
registering: false
}),
created () {
- if (!this.$store.state.config.registrationOpen || !!this.$store.state.users.currentUser) {
+ if ((!this.$store.state.config.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
this.$router.push('/main/all')
}
+ // Seems like this doesn't work at first page open for some reason
+ if (this.$store.state.config.registrationOpen && this.token) {
+ this.$router.push('/registration')
+ }
},
computed: {
- termsofservice () { return this.$store.state.config.tos }
+ termsofservice () { return this.$store.state.config.tos },
+ token () { return this.$route.params.token }
},
methods: {
submit () {
this.registering = true
this.user.nickname = this.user.username
+ this.user.token = this.token
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => {
if (response.ok) {
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 00f665af..087cab6b 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -38,6 +38,10 @@
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
</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>
<div class='form-group'>
<button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
</div>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index 9833e8b2..cafa9cbc 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,5 +1,5 @@
const RetweetButton = {
- props: ['status', 'loggedIn'],
+ props: ['status', 'loggedIn', 'visibility'],
data () {
return {
animated: false
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 1bee3d08..f5b00599 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,9 +1,9 @@
<template>
- <div v-if="loggedIn">
+ <div v-if="loggedIn && visibility !== 'private' && visibility !== 'direct'">
<i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
</div>
- <div v-else>
+ <div v-else-if="!loggedIn">
<i :class='classes' class='icon-retweet'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
</div>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index a26111d6..c85ef59f 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -1,3 +1,4 @@
+/* eslint-env browser */
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import { filter, trim } from 'lodash'
@@ -7,11 +8,22 @@ const settings = {
hideAttachmentsLocal: this.$store.state.config.hideAttachments,
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
hideNsfwLocal: this.$store.state.config.hideNsfw,
+ loopVideoLocal: this.$store.state.config.loopVideo,
+ loopVideoSilentOnlyLocal: this.$store.state.config.loopVideoSilentOnly,
muteWordsString: this.$store.state.config.muteWords.join('\n'),
autoLoadLocal: this.$store.state.config.autoLoad,
streamingLocal: this.$store.state.config.streaming,
+ pauseOnUnfocusedLocal: this.$store.state.config.pauseOnUnfocused,
hoverPreviewLocal: this.$store.state.config.hoverPreview,
- stopGifs: this.$store.state.config.stopGifs
+ collapseMessageWithSubjectLocal: this.$store.state.config.collapseMessageWithSubject,
+ stopGifs: this.$store.state.config.stopGifs,
+ loopSilentAvailable:
+ // Firefox
+ Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
+ // Chrome-likes
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
+ // Future spec, still not supported in Nightly 63 as of 08/2018
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
@@ -32,12 +44,21 @@ const settings = {
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
+ loopVideoLocal (value) {
+ this.$store.dispatch('setOption', { name: 'loopVideo', value })
+ },
+ loopVideoSilentOnlyLocal (value) {
+ this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value })
+ },
autoLoadLocal (value) {
this.$store.dispatch('setOption', { name: 'autoLoad', value })
},
streamingLocal (value) {
this.$store.dispatch('setOption', { name: 'streaming', value })
},
+ pauseOnUnfocusedLocal (value) {
+ this.$store.dispatch('setOption', { name: 'pauseOnUnfocused', value })
+ },
hoverPreviewLocal (value) {
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
},
@@ -45,6 +66,9 @@ const settings = {
value = filter(value.split('\n'), (word) => trim(word).length > 0)
this.$store.dispatch('setOption', { name: 'muteWords', value })
},
+ collapseMessageWithSubjectLocal (value) {
+ this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
+ },
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
}
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 6245e758..415317f0 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -1,53 +1,81 @@
<template>
- <div class="settings panel panel-default">
- <div class="panel-heading">
- {{$t('settings.settings')}}
+<div class="settings panel panel-default">
+ <div class="panel-heading">
+ {{$t('settings.settings')}}
+ </div>
+ <div class="panel-body">
+ <div class="setting-item">
+ <h2>{{$t('settings.theme')}}</h2>
+ <style-switcher></style-switcher>
</div>
- <div class="panel-body">
- <div class="setting-item">
- <h2>{{$t('settings.theme')}}</h2>
- <style-switcher></style-switcher>
- </div>
- <div class="setting-item">
- <h2>{{$t('settings.filtering')}}</h2>
- <p>{{$t('settings.filtering_explanation')}}</p>
- <textarea id="muteWords" v-model="muteWordsString"></textarea>
- </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>
- <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
- <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
- </li>
- <li>
- <input type="checkbox" id="autoload" v-model="autoLoadLocal">
- <label for="autoload">{{$t('settings.autoload')}}</label>
- </li>
- <li>
- <input type="checkbox" id="streaming" v-model="streamingLocal">
- <label for="streaming">{{$t('settings.streaming')}}</label>
- </li>
+ <div class="setting-item">
+ <h2>{{$t('settings.filtering')}}</h2>
+ <p>{{$t('settings.filtering_explanation')}}</p>
+ <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('nav.timeline')}}</h2>
+ <ul class="setting-list">
+ <li>
+ <input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
+ <label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}}</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}]">
<li>
- <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
- <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
+ <input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal">
+ <label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</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.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>
+ <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
+ <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
+ </li>
+ <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}]">
<li>
- <input type="checkbox" id="stopGifs" v-model="stopGifs">
- <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
+ <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')}}
+ </div>
</li>
- </ul>
- </div>
+ </ul>
+ </li>
+ </ul>
</div>
</div>
+</div>
</template>
<script src="./settings.js">
@@ -67,6 +95,12 @@
height: 100px;
}
+ .unavailable,
+ .unavailable i {
+ color: var(--cRed, $fallback--cRed);
+ color: $fallback--cRed;
+ }
+
.old-avatar {
width: 128px;
border-radius: $fallback--avatarRadius;
@@ -89,8 +123,12 @@
}
.setting-list {
list-style-type: none;
+ padding-left: 2em;
li {
margin-bottom: 0.5em;
}
+ .suboptions {
+ margin-top: 0.3em
+ }
}
</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 87ef90d8..9670f69d 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -6,6 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import StillImage from '../still-image/still-image.vue'
import { filter, find } from 'lodash'
+import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
const Status = {
name: 'Status',
@@ -21,25 +22,48 @@ const Status = {
'noHeading',
'inlineExpanded'
],
- data: () => ({
- replying: false,
- expanded: false,
- unmuted: false,
- userExpanded: false,
- preview: null,
- showPreview: false,
- showingTall: false
- }),
+ data () {
+ return {
+ replying: false,
+ expanded: false,
+ unmuted: false,
+ userExpanded: false,
+ preview: null,
+ showPreview: false,
+ showingTall: false,
+ expandingSubject: !this.$store.state.config.collapseMessageWithSubject
+ }
+ },
computed: {
muteWords () {
return this.$store.state.config.muteWords
},
+ repeaterClass () {
+ const user = this.statusoid.user
+ return highlightClass(user)
+ },
+ userClass () {
+ const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
+ return highlightClass(user)
+ },
+ repeaterStyle () {
+ const user = this.statusoid.user
+ const highlight = this.$store.state.config.highlight
+ return highlightStyle(highlight[user.screen_name])
+ },
+ userStyle () {
+ if (this.noHeading) return
+ const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
+ const highlight = this.$store.state.config.highlight
+ return highlightStyle(highlight[user.screen_name])
+ },
hideAttachments () {
return (this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)
},
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name },
+ retweeterHtml () { return this.statusoid.user.name_html },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
@@ -77,12 +101,27 @@ const Status = {
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
+ tallStatus () {
+ const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+ return lengthScore > 20
+ },
+ hideSubjectStatus () {
+ if (this.tallStatus && !this.$store.state.config.collapseMessageWithSubject) {
+ return false
+ }
+ return !this.expandingSubject && this.status.summary
+ },
hideTallStatus () {
+ if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
+ return false
+ }
if (this.showingTall) {
return false
}
- const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
- return lengthScore > 20
+ return this.tallStatus
+ },
+ showingMore () {
+ return this.showingTall || (this.status.summary && this.expandingSubject)
},
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
@@ -142,8 +181,16 @@ const Status = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
- toggleShowTall () {
- this.showingTall = !this.showingTall
+ toggleShowMore () {
+ if (this.showingTall) {
+ this.showingTall = false
+ } else if (this.expandingSubject) {
+ this.expandingSubject = false
+ } else if (this.hideTallStatus) {
+ this.showingTall = true
+ } else if (this.hideSubjectStatus) {
+ this.expandingSubject = true
+ }
},
replyEnter (id, event) {
this.showPreview = true
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index f88c810d..e7d5ed7a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -8,16 +8,17 @@
</div>
</template>
<template v-else>
- <div v-if="retweet && !noHeading" class="media container retweet-info">
+ <div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<StillImage v-if="retweet" class='avatar' :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
- <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
+ <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
+ <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
<i class='fa icon-retweet retweeted'></i>
{{$t('timeline.repeated')}}
</div>
</div>
- <div class="media status">
+ <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left">
<a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage class='avatar' :class="{'avatar-compact': compact}" :src="status.user.profile_image_url_original"/>
@@ -30,7 +31,8 @@
<div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading-left">
<div class="name-and-links">
- <h4 class="user-name">{{status.user.name}}</h4>
+ <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>
<span class="links">
<router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
@@ -55,8 +57,10 @@
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
- <span v-if="status.visibility"><i :class="visibilityIcon(status.visibility)"></i> </span>
- <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext"></i></a>
+ <div class="visibility-icon" v-if="status.visibility">
+ <i :class="visibilityIcon(status.visibility)"></i>
+ </div>
+ <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext-alt"></i></a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a>
</template>
@@ -72,9 +76,11 @@
</div>
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
- <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowTall">Show more</a>
- <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
- <a v-if="showingTall" href="#" class="tall-status-unhider" @click.prevent="toggleShowTall">Show less</a>
+ <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
+ <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
+ <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary" v-else></div>
+ <a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
+ <a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
<div v-if='status.attachments' class='attachments media-body'>
@@ -88,7 +94,7 @@
<i class="icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
- <retweet-button :loggedIn='loggedIn' :status='status'></retweet-button>
+ <retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
<delete-button :status='status'></delete-button>
</div>
@@ -139,6 +145,7 @@
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
+
.status {
flex: 1;
border: 0;
@@ -153,6 +160,7 @@
text-align: center;
border-width: 1px;
border-style: solid;
+
i {
font-size: 2em;
}
@@ -194,6 +202,7 @@
.media-heading {
flex-wrap: nowrap;
+ line-height: 18px;
}
.media-heading-left {
@@ -216,12 +225,22 @@
flex: 1 0;
display: flex;
flex-wrap: wrap;
- align-content: center;
+ align-items: baseline;
+
+ .user-name {
+ margin-right: .45em;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
}
+
.links {
display: flex;
- padding-top: 1px;
- margin-left: 0.2em;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
@@ -245,19 +264,25 @@
}
.media-heading-right {
+ display: inline-flex;
flex-shrink: 0;
- display: flex;
flex-wrap: nowrap;
- max-height: 1.5em;
- margin-left: 0.25em;
+ margin-left: .25em;
+ align-self: baseline;
+
.timeago {
margin-right: 0.2em;
font-size: 12px;
- padding-top: 1px;
+ align-self: last baseline;
}
- i {
+
+ > * {
margin-left: 0.2em;
}
+ a:hover i {
+ color: $fallback--fg;
+ color: var(--fg, $fallback--fg);
+ }
}
a {
@@ -287,7 +312,7 @@
}
}
- .tall-status-unhider {
+ .status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
}
@@ -315,7 +340,8 @@
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
- margin: 0 0 -0.5em 0;
+ margin: 0;
+
.avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
@@ -331,9 +357,22 @@
display: flex;
align-content: center;
flex-wrap: wrap;
+
+ .user-name {
+ font-weight: bold;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
+
i {
padding: 0 0.2em;
}
+
a {
max-width: 100%;
overflow: hidden;
@@ -427,6 +466,9 @@
.status {
display: flex;
padding: 0.6em;
+ &.is-retweet {
+ padding-top: 0.1em;
+ }
}
.status-conversation:last-child {
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 6f4845c4..95c15b49 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -5,6 +5,7 @@ export default {
return {
availableStyles: [],
selected: this.$store.state.config.theme,
+ invalidThemeImported: false,
bgColorLocal: '',
btnColorLocal: '',
textColorLocal: '',
@@ -32,25 +33,61 @@ export default {
})
},
mounted () {
- this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors.bg)
- this.btnColorLocal = rgbstr2hex(this.$store.state.config.colors.btn)
- this.textColorLocal = rgbstr2hex(this.$store.state.config.colors.fg)
- this.linkColorLocal = rgbstr2hex(this.$store.state.config.colors.link)
-
- this.redColorLocal = rgbstr2hex(this.$store.state.config.colors.cRed)
- this.blueColorLocal = rgbstr2hex(this.$store.state.config.colors.cBlue)
- this.greenColorLocal = rgbstr2hex(this.$store.state.config.colors.cGreen)
- this.orangeColorLocal = rgbstr2hex(this.$store.state.config.colors.cOrange)
-
- this.btnRadiusLocal = this.$store.state.config.radii.btnRadius || 4
- this.inputRadiusLocal = this.$store.state.config.radii.inputRadius || 4
- this.panelRadiusLocal = this.$store.state.config.radii.panelRadius || 10
- this.avatarRadiusLocal = this.$store.state.config.radii.avatarRadius || 5
- this.avatarAltRadiusLocal = this.$store.state.config.radii.avatarAltRadius || 50
- this.tooltipRadiusLocal = this.$store.state.config.radii.tooltipRadius || 2
- this.attachmentRadiusLocal = this.$store.state.config.radii.attachmentRadius || 5
+ this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii)
},
methods: {
+ exportCurrentTheme () {
+ const stringified = JSON.stringify({
+ // To separate from other random JSON files and possible future theme formats
+ _pleroma_theme_version: 1,
+ colors: this.$store.state.config.colors,
+ radii: this.$store.state.config.radii
+ }, null, 2) // Pretty-print and indent with 2 spaces
+
+ // Create an invisible link with a data url and simulate a click
+ const e = document.createElement('a')
+ e.setAttribute('download', 'pleroma_theme.json')
+ e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
+ e.style.display = 'none'
+
+ document.body.appendChild(e)
+ e.click()
+ document.body.removeChild(e)
+ },
+
+ importTheme () {
+ this.invalidThemeImported = false
+ const filePicker = document.createElement('input')
+ filePicker.setAttribute('type', 'file')
+ filePicker.setAttribute('accept', '.json')
+
+ filePicker.addEventListener('change', event => {
+ if (event.target.files[0]) {
+ // eslint-disable-next-line no-undef
+ const reader = new FileReader()
+ reader.onload = ({target}) => {
+ try {
+ const parsed = JSON.parse(target.result)
+ if (parsed._pleroma_theme_version === 1) {
+ this.normalizeLocalState(parsed.colors, parsed.radii)
+ } else {
+ // A theme from the future, spooky
+ this.invalidThemeImported = true
+ }
+ } catch (e) {
+ // This will happen both if there is a JSON syntax error or the theme is missing components
+ this.invalidThemeImported = true
+ }
+ }
+ reader.readAsText(event.target.files[0])
+ }
+ })
+
+ document.body.appendChild(filePicker)
+ filePicker.click()
+ document.body.removeChild(filePicker)
+ },
+
setCustomTheme () {
if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
// reset to picked themes
@@ -95,6 +132,26 @@ export default {
attachmentRadius: this.attachmentRadiusLocal
}})
}
+ },
+
+ normalizeLocalState (colors, radii) {
+ this.bgColorLocal = rgbstr2hex(colors.bg)
+ this.btnColorLocal = rgbstr2hex(colors.btn)
+ this.textColorLocal = rgbstr2hex(colors.fg)
+ this.linkColorLocal = rgbstr2hex(colors.link)
+
+ this.redColorLocal = rgbstr2hex(colors.cRed)
+ this.blueColorLocal = rgbstr2hex(colors.cBlue)
+ this.greenColorLocal = rgbstr2hex(colors.cGreen)
+ this.orangeColorLocal = rgbstr2hex(colors.cOrange)
+
+ this.btnRadiusLocal = radii.btnRadius || 4
+ this.inputRadiusLocal = radii.inputRadius || 4
+ this.panelRadiusLocal = radii.panelRadius || 10
+ this.avatarRadiusLocal = radii.avatarRadius || 5
+ this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
+ this.tooltipRadiusLocal = radii.tooltipRadius || 2
+ this.attachmentRadiusLocal = radii.attachmentRadius || 5
}
},
watch: {
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 7acba1dc..59bd2971 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -3,11 +3,19 @@
<div>{{$t('settings.presets')}}
<label for="style-switcher" class='select'>
<select id="style-switcher" v-model="selected" class="style-switcher">
- <option v-for="style in availableStyles" :value="style">{{style[0]}}</option>
+ <option v-for="style in availableStyles" :value="style" :style="{
+ backgroundColor: style[1],
+ color: style[3]
+ }">{{style[0]}}</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
+ <div>
+ <button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
+ <button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
+ <p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
+ </div>
<div class="color-container">
<p>{{$t('settings.theme_help')}}</p>
<div class="color-item">
@@ -131,6 +139,11 @@
margin-right: 1em;
}
+.import-warning {
+ color: $fallback--cRed;
+ color: var(--cRed, $fallback--cRed);
+}
+
.radius-container,
.color-container {
display: flex;
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index f24626f9..a651f619 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -13,7 +13,8 @@ const Timeline = {
],
data () {
return {
- paused: false
+ paused: false,
+ unfocused: false
}
},
computed: {
@@ -65,8 +66,15 @@ const Timeline = {
this.fetchFollowers()
}
},
+ mounted () {
+ if (typeof document.hidden !== 'undefined') {
+ document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
+ this.unfocused = document.hidden
+ }
+ },
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
+ if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
@@ -113,6 +121,9 @@ const Timeline = {
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses()
}
+ },
+ handleVisibilityChange () {
+ this.unfocused = document.hidden
}
},
watch: {
@@ -122,7 +133,10 @@ const Timeline = {
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
- if (window.pageYOffset < 15 && !this.paused) {
+ if (window.pageYOffset < 15 &&
+ !this.paused &&
+ !(this.unfocused && this.$store.state.config.pauseOnUnfocused)
+ ) {
this.showNewStatuses()
} else {
this.paused = true
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 6478a65f..7e3e0afe 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -7,10 +7,16 @@
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-else>
- <div :title="user.name" class="user-name">
+ <div :title="user.name" v-if="user.name_html" class="user-name">
+ <span v-html="user.name_html"></span>
+ <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
+ {{ $t('user_card.follows_you') }}
+ </span>
+ </div>
+ <div :title="user.name" v-else class="user-name">
{{ user.name }}
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
- {{ $t('user_card.follows_you') }}
+ {{ $t('user_card.follows_you') }}
</span>
</div>
<a :href="user.statusnet_profile_url" target="blank"><div class="user-screen-name">@{{ user.screen_name }}</div></a>
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index 4d4266cb..76a5577e 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -9,11 +9,6 @@ export default {
if (color) {
const rgb = hex2rgb(color)
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
- console.log(rgb)
- console.log([
- `url(${this.user.cover_photo})`,
- `linear-gradient(to bottom, ${tintColor}, ${tintColor})`
- ].join(', '))
return {
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
backgroundImage: [
@@ -37,6 +32,29 @@ export default {
dailyAvg () {
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
return Math.round(this.user.statuses_count / days)
+ },
+ userHighlightType: {
+ get () {
+ const data = this.$store.state.config.highlight[this.user.screen_name]
+ 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 })
+ } else {
+ this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })
+ }
+ }
+ },
+ userHighlightColor: {
+ get () {
+ const data = this.$store.state.config.highlight[this.user.screen_name]
+ return data && data.color
+ },
+ set (color) {
+ this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
+ }
}
},
components: {
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 09e91271..59358040 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -1,29 +1,46 @@
<template>
- <div id="heading" class="profile-panel-background" :style="headingStyle">
- <div class="panel-heading text-center">
- <div class='user-info'>
- <router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
- <i class="icon-cog usersettings"></i>
+<div id="heading" class="profile-panel-background" :style="headingStyle">
+ <div class="panel-heading text-center">
+ <div class='user-info'>
+ <router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
+ <i class="icon-cog usersettings"></i>
+ </router-link>
+ <a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
+ <i class="icon-link-ext usersettings"></i>
+ </a>
+ <div class='container'>
+ <router-link :to="{ name: 'user-profile', params: { id: user.id } }">
+ <StillImage class="avatar" :src="user.profile_image_url_original"/>
</router-link>
- <a :href="user.statusnet_profile_url" target="_blank" style="float: right; margin-top:16px;" v-if="isOtherUser">
- <i class="icon-link-ext usersettings"></i>
- </a>
- <div class='container'>
- <router-link :to="{ name: 'user-profile', params: { id: user.id } }">
- <StillImage class="avatar" :src="user.profile_image_url_original"/>
+ <div class="name-and-screen-name">
+ <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
+ <div :title="user.name" class='user-name' v-else>{{user.name}}</div>
+ <router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
+ <span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
+ <span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
- <div class="name-and-screen-name">
- <div :title="user.name" class='user-name'>{{user.name}}</div>
- <router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
- <span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
- <span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
- </router-link>
- </div>
</div>
- <div v-if="isOtherUser" class="user-interactions">
- <div v-if="user.follows_you && loggedIn" class="following">
- {{ $t('user_card.follows_you') }}
- </div>
+ </div>
+ <div class="user-meta">
+ <div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
+ {{ $t('user_card.follows_you') }}
+ </div>
+ <div class="floater" v-if="switcher || isOtherUser">
+ <!-- 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>
+ </div>
+ <div v-if="isOtherUser" class="user-interactions">
<div class="follow" v-if="loggedIn">
<span v-if="user.following">
<!--Following them!-->
@@ -88,7 +105,8 @@
<span>{{user.followers_count}}</span>
</div>
</div>
- <p v-if="!hideBio">{{user.description}}</p>
+ <p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
+ <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
</div>
</div>
</template>
@@ -112,7 +130,11 @@
.profile-panel-body {
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%)
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
+
+ .profile-bio {
+ text-align: center;
+ }
}
.user-info {
@@ -179,6 +201,27 @@
padding-right: 0.1em;
}
+ .user-meta {
+ margin-bottom: .4em;
+
+ .following {
+ font-size: 14px;
+ flex: 0 0 100%;
+ margin: 0;
+ padding-left: 16px;
+ text-align: left;
+ float: left;
+ }
+ .floater {
+ margin: 0;
+ }
+
+ &::after {
+ display: block;
+ content: '';
+ clear: both;
+ }
+ }
.user-interactions {
display: flex;
flex-flow: row wrap;
@@ -188,14 +231,6 @@
flex: 1;
}
- .following {
- font-size: 14px;
- flex: 0 0 100%;
- margin: 0 0 .4em 0;
- padding-left: 16px;
- text-align: left;
- }
-
.mute {
max-width: 220px;
min-height: 28px;
@@ -278,4 +313,33 @@
font-size: 0.7em;
color: #CCC;
}
+.floater {
+ float: right;
+ margin-top: 16px;
+
+ .userHighlightCl {
+ padding: 2px 10px;
+ }
+ .userHighlightSel,
+ .userHighlightSel.select {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ .userHighlightSel.select i {
+ line-height: 22px;
+ }
+
+ .userHighlightText {
+ width: 70px;
+ }
+
+ .userHighlightCl,
+ .userHighlightText,
+ .userHighlightSel,
+ .userHighlightSel.select {
+ height: 22px;
+ vertical-align: top;
+ margin-right: 0
+ }
+}
</style>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 443e63dd..f046885e 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -6,6 +6,7 @@ const UserSettings = {
newname: this.$store.state.users.currentUser.name,
newbio: this.$store.state.users.currentUser.description,
newlocked: this.$store.state.users.currentUser.locked,
+ newdefaultScope: this.$store.state.users.currentUser.default_scope,
followList: null,
followImportError: false,
followsImported: false,
@@ -17,7 +18,8 @@ const UserSettings = {
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
- changePasswordError: false
+ changePasswordError: false,
+ activeTab: 'profile'
}
},
components: {
@@ -29,6 +31,17 @@ const UserSettings = {
},
pleromaBackend () {
return this.$store.state.config.pleromaBackend
+ },
+ scopeOptionsEnabled () {
+ return this.$store.state.config.scopeOptionsEnabled
+ },
+ vis () {
+ return {
+ public: { selected: this.newdefaultScope === 'public' },
+ unlisted: { selected: this.newdefaultScope === 'unlisted' },
+ private: { selected: this.newdefaultScope === 'private' },
+ direct: { selected: this.newdefaultScope === 'direct' }
+ }
}
},
methods: {
@@ -36,12 +49,18 @@ const UserSettings = {
const name = this.newname
const description = this.newbio
const locked = this.newlocked
- this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked}}).then((user) => {
+ /* eslint-disable camelcase */
+ const default_scope = this.newdefaultScope
+ this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope}}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
+ /* eslint-enable camelcase */
+ },
+ changeVis (visibility) {
+ this.newdefaultScope = visibility
},
uploadFile (slot, e) {
const file = e.target.files[0]
@@ -217,6 +236,9 @@ const UserSettings = {
this.changePasswordError = res.error
}
})
+ },
+ activateTab (tabName) {
+ this.activeTab = tabName
}
}
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 881b0fa1..c3ca1dbd 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -4,19 +4,33 @@
{{$t('settings.user_settings')}}
</div>
<div class="panel-body profile-edit">
- <div class="setting-item">
+ <div class="tab-switcher">
+ <button class="btn btn-default" @click="activateTab('profile')">{{$t('settings.profile_tab')}}</button>
+ <button class="btn btn-default" @click="activateTab('security')">{{$t('settings.security_tab')}}</button>
+ <button class="btn btn-default" @click="activateTab('data_import_export')" v-if="pleromaBackend">{{$t('settings.data_import_export_tab')}}</button>
+ </div>
+ <div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
<input class='name-changer' id='username' v-model="newname"></input>
<p>{{$t('settings.bio')}}</p>
<textarea class="bio" v-model="newbio"></textarea>
- <div class="setting-item">
+ <p>
<input type="checkbox" v-model="newlocked" id="account-locked">
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
+ </p>
+ <div v-if="scopeOptionsEnabled">
+ <label for="default-vis">{{$t('settings.default_vis')}}</label>
+ <div id="default-vis" class="visibility-tray">
+ <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i>
+ <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i>
+ <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i>
+ <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i>
+ </div>
</div>
<button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div>
- <div class="setting-item">
+ <div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.avatar')}}</h2>
<p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img>
@@ -29,7 +43,7 @@
<i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
<button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
</div>
- <div class="setting-item">
+ <div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.profile_banner')}}</h2>
<p>{{$t('settings.current_profile_banner')}}</p>
<img :src="user.cover_photo" class="banner"></img>
@@ -42,7 +56,7 @@
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
<button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
</div>
- <div class="setting-item">
+ <div class="setting-item" v-if="activeTab == 'profile'">
<h2>{{$t('settings.profile_background')}}</h2>
<p>{{$t('settings.set_new_profile_background')}}</p>
<img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
@@ -53,7 +67,7 @@
<i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
<button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
</div>
- <div class="setting-item">
+ <div class="setting-item" v-if="activeTab == 'security'">
<h2>{{$t('settings.change_password')}}</h2>
<div>
<p>{{$t('settings.current_password')}}</p>
@@ -72,7 +86,7 @@
<p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
<p v-if="changePasswordError">{{changePasswordError}}</p>
</div>
- <div class="setting-item" v-if="pleromaBackend">
+ <div class="setting-item" v-if="pleromaBackend && activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
<form v-model="followImportForm">
@@ -89,15 +103,15 @@
<p>{{$t('settings.follow_import_error')}}</p>
</div>
</div>
- <div class="setting-item" v-if="enableFollowsExport">
+ <div class="setting-item" v-if="enableFollowsExport && activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_export')}}</h2>
<button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
</div>
- <div class="setting-item" v-else>
+ <div class="setting-item" v-else-if="activeTab == 'data_import_export'">
<h2>{{$t('settings.follow_export_processing')}}</h2>
</div>
<hr>
- <div class="setting-item">
+ <div class="setting-item" v-if="activeTab == 'security'">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
@@ -137,4 +151,13 @@
margin: 0.25em;
}
}
+
+.tab-switcher {
+ margin: 7px 7px;
+ display: inline-block;
+
+ button {
+ height: 30px;
+ }
+}
</style>
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index abaea258..04460ecc 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -48,6 +48,9 @@ const de = {
settings: 'Einstellungen',
theme: 'Farbschema',
presets: 'Voreinstellungen',
+ export_theme: 'Aktuelles Theme exportieren',
+ import_theme: 'Gespeichertes Theme laden',
+ invalid_theme_imported: 'Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.',
theme_help: 'Benutze HTML Farbcodes (#rrggbb) um dein Farbschema anzupassen',
radii_help: 'Kantenrundung (in Pixel) der Oberfläche anpassen',
background: 'Hintergrund',
@@ -288,7 +291,10 @@ const en = {
settings: 'Settings',
theme: 'Theme',
presets: 'Presets',
+ export_theme: 'Export current theme',
+ import_theme: 'Load saved theme',
theme_help: 'Use hex color codes (#rrggbb) to customize your color theme.',
+ invalid_theme_imported: 'The selected file is not a supported Pleroma theme. No changes to your theme were made.',
radii_help: 'Set up interface edge rounding (in pixels)',
background: 'Background',
foreground: 'Foreground',
@@ -311,9 +317,13 @@ const en = {
hide_attachments_in_tl: 'Hide attachments in timeline',
hide_attachments_in_convo: 'Hide attachments in conversations',
nsfw_clickthrough: 'Enable clickthrough NSFW attachment hiding',
+ collapse_subject: 'Collapse posts with subjects',
stop_gifs: 'Play-on-hover GIFs',
autoload: 'Enable automatic loading when scrolled to the bottom',
streaming: 'Enable automatic streaming of new posts when scrolled to the top',
+ pause_on_unfocused: 'Pause streaming when tab is not focused',
+ loop_video: 'Loop videos',
+ loop_video_silent_only: 'Loop only videos without sound (i.e. Mastodon\'s "gifs")',
reply_link_preview: 'Enable reply-link preview on mouse hover',
follow_import: 'Follow import',
import_followers_from_a_csv_file: 'Import follows from a csv file',
@@ -332,7 +342,12 @@ const en = {
confirm_new_password: 'Confirm new password',
changed_password: 'Password changed successfully!',
change_password_error: 'There was an issue changing your password.',
- lock_account_description: 'Restrict your account to approved followers only'
+ lock_account_description: 'Restrict your account to approved followers only',
+ limited_availability: 'Unavailable in your browser',
+ default_vis: 'Default visibility scope',
+ profile_tab: 'Profile',
+ security_tab: 'Security',
+ data_import_export_tab: 'Data Import / Export'
},
notifications: {
notifications: 'Notifications',
@@ -354,7 +369,8 @@ const en = {
fullname: 'Display name',
email: 'Email',
bio: 'Bio',
- password_confirm: 'Password confirmation'
+ password_confirm: 'Password confirmation',
+ token: 'Invite token'
},
post_status: {
posting: 'Posting',
@@ -1613,6 +1629,8 @@ const ru = {
set_new_profile_background: 'Загрузить новый фон профиля',
settings: 'Настройки',
theme: 'Тема',
+ export_theme: 'Экспортировать текущую тему',
+ import_theme: 'Загрузить сохранённую тему',
presets: 'Пресеты',
theme_help: 'Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.',
radii_help: 'Округление краёв элементов интерфейса (в пикселях)',
@@ -1640,6 +1658,9 @@ const ru = {
nsfw_clickthrough: 'Включить скрытие NSFW вложений',
autoload: 'Включить автоматическую загрузку при прокрутке вниз',
streaming: 'Включить автоматическую загрузку новых сообщений при прокрутке вверх',
+ pause_on_unfocused: 'Приостановить загрузку когда вкладка не в фокусе',
+ loop_video: 'Зациливать видео',
+ loop_video_silent_only: 'Зацикливать только беззвучные видео (т.е. "гифки" с Mastodon)',
reply_link_preview: 'Включить предварительный просмотр ответа при наведении мыши',
follow_import: 'Импортировать читаемых',
import_followers_from_a_csv_file: 'Импортировать читаемых из файла .csv',
@@ -1657,7 +1678,13 @@ const ru = {
new_password: 'Новый пароль',
confirm_new_password: 'Подтверждение нового пароля',
changed_password: 'Пароль изменён успешно.',
- change_password_error: 'Произошла ошибка при попытке изменить пароль.'
+ change_password_error: 'Произошла ошибка при попытке изменить пароль.',
+ lock_account_description: 'Аккаунт доступен только подтверждённым подписчикам',
+ limited_availability: 'Не доступно в вашем браузере',
+ profile_tab: 'Профиль',
+ security_tab: 'Безопасность',
+ data_import_export_tab: 'Импорт / Экспорт данных',
+ collapse_subject: 'Сворачивать посты с темой'
},
notifications: {
notifications: 'Уведомления',
@@ -1679,7 +1706,8 @@ const ru = {
fullname: 'Отображаемое имя',
email: 'Email',
bio: 'Описание',
- password_confirm: 'Подтверждение пароля'
+ password_confirm: 'Подтверждение пароля',
+ token: 'Код приглашения'
},
post_status: {
posting: 'Отправляется',
diff --git a/src/main.js b/src/main.js
index bacd7f6d..06f8a6ec 100644
--- a/src/main.js
+++ b/src/main.js
@@ -45,6 +45,7 @@ Vue.use(VueChatScroll)
const persistedStateOptions = {
paths: [
+ 'config.collapseMessageWithSubject',
'config.hideAttachments',
'config.hideAttachmentsInConv',
'config.hideNsfw',
@@ -53,6 +54,11 @@ const persistedStateOptions = {
'config.streaming',
'config.muteWords',
'config.customTheme',
+ 'config.highlight',
+ 'config.loopVideo',
+ 'config.loopVideoSilentOnly',
+ 'config.pauseOnUnfocused',
+ 'config.stopGifs',
'users.lastLoginName'
]
}
@@ -79,17 +85,18 @@ const i18n = new VueI18n({
window.fetch('/api/statusnet/config.json')
.then((res) => res.json())
.then((data) => {
- const {name, closed: registrationClosed, textlimit} = data.site
+ const {name, closed: registrationClosed, textlimit, server} = data.site
store.dispatch('setOption', { name: 'name', value: name })
store.dispatch('setOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setOption', { name: 'textlimit', value: parseInt(textlimit) })
+ store.dispatch('setOption', { name: 'server', value: server })
})
window.fetch('/static/config.json')
.then((res) => res.json())
.then((data) => {
- const {theme, background, logo, showWhoToFollowPanel, whoToFollowProvider, whoToFollowLink, showInstanceSpecificPanel, scopeOptionsEnabled} = data
+ const {theme, background, logo, showWhoToFollowPanel, whoToFollowProvider, whoToFollowLink, showInstanceSpecificPanel, scopeOptionsEnabled, collapseMessageWithSubject} = data
store.dispatch('setOption', { name: 'theme', value: theme })
store.dispatch('setOption', { name: 'background', value: background })
store.dispatch('setOption', { name: 'logo', value: logo })
@@ -98,6 +105,7 @@ window.fetch('/static/config.json')
store.dispatch('setOption', { name: 'whoToFollowLink', value: whoToFollowLink })
store.dispatch('setOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
store.dispatch('setOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
+ store.dispatch('setOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject })
if (data['chatDisabled']) {
store.dispatch('disableChat')
}
@@ -119,6 +127,7 @@ window.fetch('/static/config.json')
{ name: 'mentions', path: '/:username/mentions', component: Mentions },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
+ { name: 'registration', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }
]
diff --git a/src/modules/config.js b/src/modules/config.js
index 9a62905e..60210a95 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,16 +1,22 @@
-import { set } from 'vue'
+import { set, delete as del } from 'vue'
import StyleSetter from '../services/style_setter/style_setter.js'
const defaultState = {
name: 'Pleroma FE',
colors: {},
+ collapseMessageWithSubject: false,
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,
+ loopVideo: true,
+ loopVideoSilentOnly: true,
autoLoad: true,
streaming: false,
hoverPreview: true,
- muteWords: []
+ pauseOnUnfocused: true,
+ stopGifs: false,
+ muteWords: [],
+ highlight: {}
}
const config = {
@@ -18,12 +24,23 @@ const config = {
mutations: {
setOption (state, { name, value }) {
set(state, name, value)
+ },
+ setHighlight (state, { user, color, type }) {
+ const data = this.state.config.highlight[user]
+ if (color || type) {
+ set(state.highlight, user, { color: color || data.color, type: type || data.type })
+ } else {
+ del(state.highlight, user)
+ }
}
},
actions: {
setPageTitle ({state}, option = '') {
document.title = `${option} ${state.name}`
},
+ setHighlight ({ commit, dispatch }, { user, color, type }) {
+ commit('setHighlight', {user, color, type})
+ },
setOption ({ commit, dispatch }, { name, value }) {
commit('setOption', {name, value})
switch (name) {
diff --git a/src/modules/users.js b/src/modules/users.js
index 8303ecc1..ba548765 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -42,6 +42,10 @@ export const mutations = {
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
+ },
+ setColor (state, { user: {id}, highlighted }) {
+ const user = state.usersObject[id]
+ set(user, 'highlight', highlighted)
}
}
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index adf598b7..13cc4796 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -159,6 +159,7 @@ const updateProfile = ({credentials, params}) => {
// bio
// homepage
// location
+// token
const register = (params) => {
const form = new FormData()
diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js
new file mode 100644
index 00000000..ebb25eca
--- /dev/null
+++ b/src/services/user_highlighter/user_highlighter.js
@@ -0,0 +1,48 @@
+import { hex2rgb } from '../color_convert/color_convert.js'
+const highlightStyle = (prefs) => {
+ if (prefs === undefined) return
+ const {color, type} = prefs
+ if (typeof color !== 'string') return
+ const rgb = hex2rgb(color)
+ if (rgb == null) return
+ const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
+ const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
+ const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
+ if (type === 'striped') {
+ return {
+ backgroundImage: [
+ 'repeating-linear-gradient(-45deg,',
+ `${tintColor} ,`,
+ `${tintColor} 20px,`,
+ `${tintColor2} 20px,`,
+ `${tintColor2} 40px`
+ ].join(' '),
+ backgroundPosition: '0 0'
+ }
+ } else if (type === 'solid') {
+ return {
+ backgroundColor: tintColor2
+ }
+ } else if (type === 'side') {
+ return {
+ backgroundImage: [
+ 'linear-gradient(to right,',
+ `${solidColor} ,`,
+ `${solidColor} 2px,`,
+ `transparent 6px`
+ ].join(' '),
+ backgroundPosition: '0 0'
+ }
+ }
+}
+
+const highlightClass = (user) => {
+ return 'USER____' + user.screen_name
+ .replace(/\./g, '_')
+ .replace(/@/g, '_AT_')
+}
+
+export {
+ highlightClass,
+ highlightStyle
+}