diff options
| author | Henry Jameson <me@hjkos.com> | 2018-08-20 20:04:54 +0300 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2018-08-20 20:04:54 +0300 |
| commit | 35b912bce4e883b4ed33b3d71c5b5626a7f1a492 (patch) | |
| tree | cec24707cc496487c00de9d39fe4e8f1608e8d14 /src | |
| parent | 9e78c64d5eecb5f73e4da401dd3baec94e77efd7 (diff) | |
| parent | 4ba4772e921ce0600e62e116bbc8e68686e51f32 (diff) | |
Merge remote-tracking branch 'upstream/develop' into notifications
* upstream/develop: (23 commits)
Rename expandCW to collapseMessageWithSubject.
fix indent
Add support for configurable CW clickthrough.
Merge upstream
fix lint issues
allow default visibility scope to be configured
Revert "storing entire config instead of each separate thing of it, so that future"
fixes hella ton of annoyances with file upload display
using custom ascend value as suggested here: https://github.com/fontello/fontello/issues/513#issuecomment-237551101 helped.
disable hinting because it breaks alignment on some icons (namely - locks)
fix for timeago being ass when post has replies. added hover colors for clickable icons on the right side. Reverted line-height to its original value
Configurable video looping, option to not to loop silent videos. Updated localization strings.
added pointer cursor for nsfw placeholder. fixed nsfw videos requiring double-click
Made pausing TL updating configurable. Added styles for disabled checkboxes. Shuffled settings a bit b/c all the settings are in "Attachments" section depsite the fact not all of them are attachments-related.
storing entire config instead of each separate thing of it, so that future options won't be lost during reloads because developer forgot to update that list of settings to be persisted
fix potential stretched spurdo
fixed custom emoji in nickname. changed icons on right side to be more streamlined. adjusted CSS so that all text in header of post is on same baseline and all icons/images are middle-aligned.
Add validation of the imported theme and the corresponding warning message
Unify button styles and use min-width
Add German localization for theme import/export
...
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.scss | 30 | ||||
| -rw-r--r-- | src/components/attachment/attachment.js | 38 | ||||
| -rw-r--r-- | src/components/attachment/attachment.vue | 9 | ||||
| -rw-r--r-- | src/components/login_form/login_form.vue | 5 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.js | 2 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.vue | 72 | ||||
| -rw-r--r-- | src/components/settings/settings.js | 26 | ||||
| -rw-r--r-- | src/components/settings/settings.vue | 124 | ||||
| -rw-r--r-- | src/components/status/status.js | 52 | ||||
| -rw-r--r-- | src/components/status/status.vue | 69 | ||||
| -rw-r--r-- | src/components/style_switcher/style_switcher.js | 91 | ||||
| -rw-r--r-- | src/components/style_switcher/style_switcher.vue | 10 | ||||
| -rw-r--r-- | src/components/timeline/timeline.js | 5 | ||||
| -rw-r--r-- | src/components/user_settings/user_settings.js | 26 | ||||
| -rw-r--r-- | src/components/user_settings/user_settings.vue | 43 | ||||
| -rw-r--r-- | src/i18n/messages.js | 23 | ||||
| -rw-r--r-- | src/main.js | 8 | ||||
| -rw-r--r-- | src/modules/config.js | 5 |
18 files changed, 485 insertions, 153 deletions
diff --git a/src/App.scss b/src/App.scss index 2426b998..ae6561a9 100644 --- a/src/App.scss +++ b/src/App.scss @@ -63,6 +63,8 @@ button{ box-shadow: 0px 0px 2px black; font-size: 14px; font-family: sans-serif; + min-width: 10em; + min-height: 2em; &:hover { box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3); @@ -142,6 +144,14 @@ input, textarea, .select { color: $fallback--fg; color: var(--fg, $fallback--fg); } + &:disabled, + { + &, + & + label, + & + label::before { + opacity: .5; + } + } + label::before { display: inline-block; content: '✔'; @@ -433,3 +443,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/login_form/login_form.vue b/src/components/login_form/login_form.vue index b7fed48a..d2bdffcb 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -34,11 +34,6 @@ @import '../../_variables.scss'; .login-form { - .btn { - min-height: 28px; - width: 10em; - } - .error { text-align: center; } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 61f2ac0a..ff3bb906 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -54,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 } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 7aa0e7c4..1e1c6f1d 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,35 +101,12 @@ } } -.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; padding: 0.5em; height: 32px; - button { - width: 10em; - } - p { margin: 0.35em; padding: 0.35em; @@ -139,14 +118,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/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..170f5773 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; @@ -83,14 +117,16 @@ .btn { margin-top: 1em; - min-height: 28px; - width: 10em; } } .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 a2d6f41f..9670f69d 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -22,15 +22,18 @@ 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 @@ -98,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) || @@ -163,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 e2fb5d36..e7d5ed7a 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -11,8 +11,8 @@ <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 v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a> - <a v-else :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> @@ -57,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> @@ -74,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'> @@ -141,6 +145,7 @@ margin-top: 0.25em; margin-left: 0.5em; z-index: 50; + .status { flex: 1; border: 0; @@ -155,6 +160,7 @@ text-align: center; border-width: 1px; border-style: solid; + i { font-size: 2em; } @@ -196,6 +202,7 @@ .media-heading { flex-wrap: nowrap; + line-height: 18px; } .media-heading-left { @@ -218,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); @@ -247,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 { @@ -289,7 +312,7 @@ } } - .tall-status-unhider { + .status-unhider, .cw-status-hider { width: 100%; text-align: center; } @@ -318,6 +341,7 @@ .retweet-info { padding: 0.4em 0.6em 0 0.6em; margin: 0; + .avatar { border-radius: $fallback--avatarAltRadius; border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); @@ -333,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; 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 112bbc1e..59bd2971 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -11,6 +11,11 @@ <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"> @@ -134,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 5c179567..a651f619 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -133,7 +133,10 @@ const Timeline = { } if (count > 0) { // only 'stream' them when you're scrolled to the top - if (window.pageYOffset < 15 && !this.paused && !this.unfocused) { + 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_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 0bcfb25a..00c56254 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', @@ -1607,6 +1622,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', @@ -1624,7 +1642,8 @@ const ru = { new_password: 'Новый пароль', confirm_new_password: 'Подтверждение нового пароля', changed_password: 'Пароль изменён успешно.', - change_password_error: 'Произошла ошибка при попытке изменить пароль.' + change_password_error: 'Произошла ошибка при попытке изменить пароль.', + limited_availability: 'Не доступно в вашем браузере' }, notifications: { notifications: 'Уведомления', diff --git a/src/main.js b/src/main.js index e344cb66..d4fbaa7b 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', @@ -54,6 +55,10 @@ const persistedStateOptions = { 'config.muteWords', 'config.customTheme', 'config.highlight', + 'config.loopVideo', + 'config.loopVideoSilentOnly', + 'config.pauseOnUnfocused', + 'config.stopGifs', 'users.lastLoginName', 'statuses.notifications.maxSavedId' ] @@ -92,7 +97,7 @@ window.fetch('/api/statusnet/config.json') 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 }) @@ -101,6 +106,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') } diff --git a/src/modules/config.js b/src/modules/config.js index 2b50655b..60210a95 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -4,12 +4,17 @@ 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, + pauseOnUnfocused: true, + stopGifs: false, muteWords: [], highlight: {} } |
