diff options
Diffstat (limited to 'src')
29 files changed, 477 insertions, 142 deletions
@@ -68,10 +68,14 @@ export default { logo () { return this.$store.state.instance.logo }, bgStyle () { return { - '--body-background-image': `url(${this.background})`, 'background-image': `url(${this.background})` } }, + bgAppStyle () { + return { + '--body-background-image': `url(${this.background})` + } + }, sitename () { return this.$store.state.instance.name }, chat () { return this.$store.state.chat.channel.state === 'joined' }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, diff --git a/src/App.scss b/src/App.scss index 52484f59..7c6970c1 100644 --- a/src/App.scss +++ b/src/App.scss @@ -181,8 +181,7 @@ input, textarea, .select { color: $fallback--text; color: var(--text, $fallback--text); } - &:disabled, - { + &:disabled { &, & + label, & + label::before { @@ -649,10 +648,6 @@ nav { color: var(--lightText, $fallback--lightText); } - .text-format { - float: right; - } - div { padding-top: 5px; } @@ -739,3 +734,7 @@ nav { width: 100%; } } + +.btn.btn-default { + min-height: 28px; +} diff --git a/src/App.vue b/src/App.vue index fa5736e5..acbbeb75 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@ <template> - <div id="app"> + <div id="app" v-bind:style="bgAppStyle"> <div class="app-bg-wrapper" v-bind:style="bgStyle"></div> <nav class='nav-bar container' @click="scrollToTop()" id="nav"> <div class='logo' :style='logoBgStyle'> diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue index bf65efc5..b37469ac 100644 --- a/src/components/chat_panel/chat_panel.vue +++ b/src/components/chat_panel/chat_panel.vue @@ -3,8 +3,8 @@ <div class="panel panel-default"> <div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel"> <div class="title"> - {{$t('chat.title')}} - <i class="icon-cancel" style="float: right;" v-if="floating"></i> + <span>{{$t('chat.title')}}</span> + <i class="icon-cancel" v-if="floating"></i> </div> </div> <div class="chat-window" v-chat-scroll> @@ -98,4 +98,11 @@ resize: none; } } + +.chat-panel { + .title { + display: flex; + justify-content: space-between; + } +} </style> diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js index 45b39f42..9777c87e 100644 --- a/src/components/follow_list/follow_list.js +++ b/src/components/follow_list/follow_list.js @@ -26,7 +26,9 @@ const FollowList = { entries () { return this.showFollowers ? this.user.followers : this.user.friends }, - showActions () { return this.$store.state.users.currentUser.id === this.userId } + showFollowsYou () { + return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id) + } }, methods: { fetchEntries () { diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue index 7be2e7b7..27102edf 100644 --- a/src/components/follow_list/follow_list.vue +++ b/src/components/follow_list/follow_list.vue @@ -3,8 +3,7 @@ <user-card v-for="entry in entries" :key="entry.id" :user="entry" - :showFollows="!showFollowers" - :showActions="showActions" + :noFollowsYou="!showFollowsYou" /> <div class="text-center panel-footer"> <a v-if="error" @click="fetchEntries" class="alert error"> diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js new file mode 100644 index 00000000..990c0370 --- /dev/null +++ b/src/components/image_cropper/image_cropper.js @@ -0,0 +1,128 @@ +import Cropper from 'cropperjs' +import 'cropperjs/dist/cropper.css' + +const ImageCropper = { + props: { + trigger: { + type: [String, window.Element], + required: true + }, + submitHandler: { + type: Function, + required: true + }, + cropperOptions: { + type: Object, + default () { + return { + aspectRatio: 1, + autoCropArea: 1, + viewMode: 1, + movable: false, + zoomable: false, + guides: false + } + } + }, + mimes: { + type: String, + default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon' + }, + saveButtonLabel: { + type: String + }, + cancelButtonLabel: { + type: String + } + }, + data () { + return { + cropper: undefined, + dataUrl: undefined, + filename: undefined, + submitting: false, + submitError: null + } + }, + computed: { + saveText () { + return this.saveButtonLabel || this.$t('image_cropper.save') + }, + cancelText () { + return this.cancelButtonLabel || this.$t('image_cropper.cancel') + }, + submitErrorMsg () { + return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError + } + }, + methods: { + destroy () { + if (this.cropper) { + this.cropper.destroy() + } + this.$refs.input.value = '' + this.dataUrl = undefined + this.$emit('close') + }, + submit () { + this.submitting = true + this.avatarUploadError = null + this.submitHandler(this.cropper, this.filename) + .then(() => this.destroy()) + .catch((err) => { + this.submitError = err + }) + .finally(() => { + this.submitting = false + }) + }, + pickImage () { + this.$refs.input.click() + }, + createCropper () { + this.cropper = new Cropper(this.$refs.img, this.cropperOptions) + }, + getTriggerDOM () { + return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger) + }, + readFile () { + const fileInput = this.$refs.input + if (fileInput.files != null && fileInput.files[0] != null) { + let reader = new window.FileReader() + reader.onload = (e) => { + this.dataUrl = e.target.result + this.$emit('open') + } + reader.readAsDataURL(fileInput.files[0]) + this.filename = fileInput.files[0].name || 'unknown' + this.$emit('changed', fileInput.files[0], reader) + } + }, + clearError () { + this.submitError = null + } + }, + mounted () { + // listen for click event on trigger + const trigger = this.getTriggerDOM() + if (!trigger) { + this.$emit('error', 'No image make trigger found.', 'user') + } else { + trigger.addEventListener('click', this.pickImage) + } + // listen for input file changes + const fileInput = this.$refs.input + fileInput.addEventListener('change', this.readFile) + }, + beforeDestroy: function () { + // remove the event listeners + const trigger = this.getTriggerDOM() + if (trigger) { + trigger.removeEventListener('click', this.pickImage) + } + const fileInput = this.$refs.input + fileInput.removeEventListener('change', this.readFile) + } +} + +export default ImageCropper diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue new file mode 100644 index 00000000..24a6f3bd --- /dev/null +++ b/src/components/image_cropper/image_cropper.vue @@ -0,0 +1,42 @@ +<template> + <div class="image-cropper"> + <div v-if="dataUrl"> + <div class="image-cropper-image-container"> + <img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" /> + </div> + <div class="image-cropper-buttons-wrapper"> + <button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button> + <button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button> + <i class="icon-spin4 animate-spin" v-if="submitting"></i> + </div> + <div class="alert error" v-if="submitError"> + {{submitErrorMsg}} + <i class="button-icon icon-cancel" @click="clearError"></i> + </div> + </div> + <input ref="input" type="file" class="image-cropper-img-input" :accept="mimes"> + </div> +</template> + +<script src="./image_cropper.js"></script> + +<style lang="scss"> +.image-cropper { + &-img-input { + display: none; + } + + &-image-container { + position: relative; + + img { + display: block; + max-width: 100%; + } + } + + &-buttons-wrapper { + margin-top: 15px; + } +} +</style> diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 14ae19d4..992d7129 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -11,27 +11,62 @@ const MediaModal = { showing () { return this.$store.state.mediaViewer.activated }, + media () { + return this.$store.state.mediaViewer.media + }, currentIndex () { return this.$store.state.mediaViewer.currentIndex }, currentMedia () { - return this.$store.state.mediaViewer.media[this.currentIndex] + return this.media[this.currentIndex] + }, + canNavigate () { + return this.media.length > 1 }, type () { return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null } }, - created () { - document.addEventListener('keyup', e => { - if (e.keyCode === 27 && this.showing) { // escape - this.hide() - } - }) - }, methods: { hide () { this.$store.dispatch('closeMediaViewer') + }, + goPrev () { + if (this.canNavigate) { + const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) + this.$store.dispatch('setCurrent', this.media[prevIndex]) + } + }, + goNext () { + if (this.canNavigate) { + const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) + this.$store.dispatch('setCurrent', this.media[nextIndex]) + } + }, + handleKeyupEvent (e) { + if (this.showing && e.keyCode === 27) { // escape + this.hide() + } + }, + handleKeydownEvent (e) { + if (!this.showing) { + return + } + + if (e.keyCode === 39) { // arrow right + this.goNext() + } else if (e.keyCode === 37) { // arrow left + this.goPrev() + } } + }, + mounted () { + document.addEventListener('keyup', this.handleKeyupEvent) + document.addEventListener('keydown', this.handleKeydownEvent) + }, + destroyed () { + document.removeEventListener('keyup', this.handleKeyupEvent) + document.removeEventListener('keydown', this.handleKeydownEvent) } } diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 796d4e40..427bf12b 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -8,6 +8,22 @@ :controls="true" @click.stop.native=""> </VideoAttachment> + <button + :title="$t('media_modal.previous')" + class="modal-view-button-arrow modal-view-button-arrow--prev" + v-if="canNavigate" + @click.stop.prevent="goPrev" + > + <i class="icon-left-open arrow-icon" /> + </button> + <button + :title="$t('media_modal.next')" + class="modal-view-button-arrow modal-view-button-arrow--next" + v-if="canNavigate" + @click.stop.prevent="goNext" + > + <i class="icon-right-open arrow-icon" /> + </button> </div> </template> @@ -19,15 +35,29 @@ .modal-view { z-index: 1000; position: fixed; - width: 100vw; - height: 100vh; top: 0; left: 0; + right: 0; + bottom: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); - cursor: pointer; + + &:hover { + .modal-view-button-arrow { + opacity: 0.75; + + &:focus, + &:hover { + outline: none; + box-shadow: none; + } + &:hover { + opacity: 1; + } + } + } } .modal-image { @@ -35,4 +65,49 @@ max-height: 90%; box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); } + +.modal-view-button-arrow { + position: absolute; + display: block; + top: 50%; + margin-top: -50px; + width: 70px; + height: 100px; + border: 0; + padding: 0; + opacity: 0; + box-shadow: none; + background: none; + appearance: none; + overflow: visible; + cursor: pointer; + transition: opacity 333ms cubic-bezier(.4,0,.22,1); + + .arrow-icon { + position: absolute; + top: 35px; + height: 30px; + width: 32px; + font-size: 14px; + line-height: 30px; + color: #FFF; + text-align: center; + background-color: rgba(0,0,0,.3); + } + + &--prev { + left: 0; + .arrow-icon { + left: 6px; + } + } + + &--next { + right: 0; + .arrow-icon { + right: 6px; + } + } +} + </style> diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 3aa0a793..1a269adf 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -19,7 +19,10 @@ </li> <li v-if='currentUser && currentUser.locked'> <router-link :to="{ name: 'friend-requests' }"> - {{ $t("nav.friend_requests") }} + {{ $t("nav.friend_requests")}} + <span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> + {{currentUser.follow_request_count}} + </span> </router-link> </li> <li> @@ -52,6 +55,12 @@ padding: 0; } +.follow-request-count { + margin: -6px 10px; + background-color: $fallback--bg; + background-color: var(--input, $fallback--faint); +} + .nav-panel li { border-bottom: 1px solid; border-color: $fallback--border; diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index bc81d45c..b3364afc 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -103,6 +103,7 @@ flex: 1 1 0; display: flex; flex-wrap: nowrap; + justify-content: space-between; .name-and-action { flex: 1; @@ -123,8 +124,8 @@ object-fit: contain } } + .timeago { - float: right; font-size: 12px; } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 6ed5d92e..b3cc0ce6 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -118,6 +118,14 @@ } } +.post-status-form { + .visibility-tray { + display: flex; + justify-content: space-between; + flex-direction: row-reverse; + } +} + .post-status-form, .login { .form-bottom { display: flex; diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 534a9839..23c1acdb 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -91,7 +91,8 @@ const settings = { }, currentSaveStateNotice () { return this.$store.state.interface.settings.currentSaveStateNotice - } + }, + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel } }, watch: { hideAttachmentsLocal (value) { diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index dfb2e49d..f5e00995 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -27,7 +27,7 @@ <li> <interface-language-switcher /> </li> - <li> + <li v-if="instanceSpecificPanelPresent"> <input type="checkbox" id="hideISP" v-model="hideISPLocal"> <label for="hideISP">{{$t('settings.hide_isp')}}</label> </li> @@ -311,20 +311,6 @@ color: $fallback--cRed; } - .old-avatar { - width: 128px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); - } - - .new-avatar { - object-fit: cover; - width: 128px; - height: 128px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); - } - .btn { min-height: 28px; min-width: 10em; diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index a6c6f237..8eca7b8c 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -45,6 +45,10 @@ <li v-if="currentUser && currentUser.locked" @click="toggleDrawer"> <router-link to='/friend-requests'> {{ $t("nav.friend_requests") }} + <span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> + {{currentUser.follow_request_count}} + </span> + </router-link> </li> <li @click="toggleDrawer"> diff --git a/src/components/status/status.vue b/src/components/status/status.vue index aae365d1..3fc5b486 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -554,7 +554,7 @@ a.unmute { .timeline > { .status-el:last-child { - border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;; + border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-bottom: none; } diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index a4c84716..28e22f09 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -6,9 +6,8 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate const UserCard = { props: [ 'user', - 'showFollows', - 'showApproval', - 'showActions' + 'noFollowsYou', + 'showApproval' ], data () { return { @@ -26,7 +25,7 @@ const UserCard = { currentUser () { return this.$store.state.users.currentUser }, following () { return this.updated ? this.updated.following : this.user.following }, showFollow () { - return this.showActions && (!this.showFollows && !this.following || this.updated && !this.updated.following) + return !this.showApproval && (!this.following || this.updated && !this.updated.following) } }, methods: { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 12960c02..ce4edb3c 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -1,27 +1,31 @@ <template> <div class="card"> <router-link :to="userProfileLink(user)"> - <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> + <UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> </router-link> - <div class="usercard" v-if="userExpanded"> - <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"> - <span v-if="user.name_html" v-html="user.name_html"></span> - <span v-else>{{ user.name }}</span> - <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you"> + <div class="user-card-main-content"> + <div class="usercard" v-if="userExpanded"> + <user-card-content :user="user" :switcher="false"></user-card-content> + </div> + <div class="name-and-screen-name" v-if="!userExpanded"> + <div :title="user.name" class="user-name"> + <span v-if="user.name_html" v-html="user.name_html"></span> + <span v-else>{{ user.name }}</span> + </div> + <div class="user-link-action"> + <router-link class='user-screen-name' :to="userProfileLink(user)"> + @{{user.screen_name}} + </router-link> + </div> + </div> + <div class="follow-box" v-if="!userExpanded"> + <span class="faint" v-if="!noFollowsYou && user.follows_you"> {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }} </span> - </div> - <div class="user-link-action"> - <router-link class='user-screen-name' :to="userProfileLink(user)"> - @{{user.screen_name}} - </router-link> - <button - v-if="showFollow" - class="btn btn-default" - @click="followUser" + <button + v-if="showFollow" + class="btn btn-default" + @click="followUser" :disabled="followRequestInProgress" :title="followRequestSent ? $t('user_card.follow_again') : ''" > @@ -35,7 +39,7 @@ {{ $t('user_card.follow') }} </template> </button> - <button v-if="showActions && showFollows && following" class="btn btn-default" @click="unfollowUser" :disabled="followRequestInProgress"> + <button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="followRequestInProgress"> <template v-if="followRequestInProgress"> {{ $t('user_card.follow_progress') }} </template> @@ -44,10 +48,10 @@ </template> </button> </div> - </div> - <div class="approval" v-if="showApproval"> - <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button> - <button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button> + <div class="approval" v-if="showApproval"> + <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button> + <button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button> + </div> </div> </div> </template> @@ -57,15 +61,19 @@ <style lang="scss"> @import '../../_variables.scss'; -.name-and-screen-name { +.user-card-main-content { + display: flex; + flex-direction: column; + flex: 1 1 100%; margin-left: 0.7em; - margin-top:0.0em; + min-width: 0; +} + +.name-and-screen-name { text-align: left; width: 100%; - .user-name { - display: flex; - justify-content: space-between; + .user-name { img { object-fit: contain; height: 16px; @@ -73,21 +81,14 @@ vertical-align: middle; } } - + .user-link-action { display: flex; align-items: flex-start; justify-content: space-between; - - button { - margin-top: 3px; - } } } -.follows-you { - margin-left: 2em; -} .card { display: flex; @@ -99,16 +100,31 @@ border-bottom: 1px solid; margin: 0; border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border, $fallback--border); .avatar { padding: 0; } + + .follow-box { + text-align: center; + flex-shrink: 0; + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; + line-height: 1.5em; + + .btn { + margin-top: 0.5em; + margin-left: auto; + width: 10em; + } + } } .usercard { width: fill-available; - margin: 0.2em 0 0 0.7em; border-radius: $fallback--panelRadius; border-radius: var(--panelRadius, $fallback--panelRadius); border-style: solid; @@ -129,9 +145,15 @@ } .approval { + display: flex; + flex-direction: row; + flex-wrap: wrap; button { - width: 100%; - margin-bottom: 0.5em; + margin-top: 0.5em; + margin-right: 0.5em; + flex: 1 1; + max-width: 12em; + min-width: 8em; } } </style> diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 7f9909c4..a3d24eb1 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -13,7 +13,7 @@ <router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser"> <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> </router-link> - <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser"> + <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local"> <i class="icon-link-ext usersettings"></i> </a> </div> @@ -386,6 +386,4 @@ } } -.floater { -} </style> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index a22b8722..37179ce1 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -16,7 +16,7 @@ const UserProfile = { } }, destroyed () { - this.cleanUp(this.userId) + this.cleanUp() }, computed: { timeline () { diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index d20bf308..dce3eeed 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -1,6 +1,7 @@ import { unescape } from 'lodash' import TabSwitcher from '../tab_switcher/tab_switcher.js' +import ImageCropper from '../image_cropper/image_cropper.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' @@ -20,14 +21,12 @@ const UserSettings = { followImportError: false, followsImported: false, enableFollowsExport: true, - avatarUploading: false, + pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, followListUploading: false, - avatarPreview: null, bannerPreview: null, backgroundPreview: null, - avatarUploadError: null, bannerUploadError: null, backgroundUploadError: null, deletingAccount: false, @@ -41,7 +40,8 @@ const UserSettings = { }, components: { StyleSwitcher, - TabSwitcher + TabSwitcher, + ImageCropper }, computed: { user () { @@ -117,35 +117,15 @@ const UserSettings = { } reader.readAsDataURL(file) }, - submitAvatar () { - if (!this.avatarPreview) { return } - - let img = this.avatarPreview - // eslint-disable-next-line no-undef - let imginfo = new Image() - let cropX, cropY, cropW, cropH - imginfo.src = img - if (imginfo.height > imginfo.width) { - cropX = 0 - cropW = imginfo.width - cropY = Math.floor((imginfo.height - imginfo.width) / 2) - cropH = imginfo.width - } else { - cropY = 0 - cropH = imginfo.height - cropX = Math.floor((imginfo.width - imginfo.height) / 2) - cropW = imginfo.height - } - this.avatarUploading = true - this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => { + submitAvatar (cropper) { + const img = cropper.getCroppedCanvas().toDataURL('image/jpeg') + return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => { if (!user.error) { this.$store.commit('addNewUsers', [user]) this.$store.commit('setCurrentUser', user) - this.avatarPreview = null } else { - this.avatarUploadError = this.$t('upload.error.base') + user.error + throw new Error(this.$t('upload.error.base') + user.error) } - this.avatarUploading = false }) }, clearUploadError (slot) { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index d2381da2..8ab92e95 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -48,19 +48,10 @@ <h2>{{$t('settings.avatar')}}</h2> <p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p> <p>{{$t('settings.current_avatar')}}</p> - <img :src="user.profile_image_url_original" class="old-avatar"></img> + <img :src="user.profile_image_url_original" class="current-avatar"></img> <p>{{$t('settings.set_new_avatar')}}</p> - <img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview"> - </img> - <div> - <input type="file" @change="uploadFile('avatar', $event)" ></input> - </div> - <i class="icon-spin4 animate-spin" v-if="avatarUploading"></i> - <button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button> - <div class='alert error' v-if="avatarUploadError"> - Error: {{ avatarUploadError }} - <i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i> - </div> + <button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button> + <image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" /> </div> <div class="setting-item"> <h2>{{$t('settings.profile_banner')}}</h2> @@ -167,6 +158,8 @@ </script> <style lang="scss"> +@import '../../_variables.scss'; + .profile-edit { .bio { margin: 0; @@ -193,5 +186,13 @@ .bg { max-width: 100%; } + + .current-avatar { + display: block; + width: 150px; + height: 150px; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + } } </style> diff --git a/src/i18n/en.json b/src/i18n/en.json index c664fbfa..78e8fced 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -21,6 +21,11 @@ "more": "More", "generic_error": "An error occured" }, + "image_cropper": { + "crop_picture": "Crop picture", + "save": "Save", + "cancel": "Cancel" + }, "login": { "login": "Log in", "description": "Log in with OAuth", @@ -31,6 +36,10 @@ "username": "Username", "hint": "Log in to join the discussion" }, + "media_modal": { + "previous": "Previous", + "next": "Next" + }, "nav": { "about": "About", "back": "Back", @@ -206,6 +215,7 @@ "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", "tooltipRadius": "Tooltips/alerts", + "upload_a_photo": "Upload a photo", "user_settings": "User Settings", "values": { "false": "no", @@ -344,7 +354,7 @@ "follow_sent": "Request sent!", "follow_progress": "Requesting…", "follow_again": "Send request again?", - "follow_unfollow": "Stop following", + "follow_unfollow": "Unfollow", "followees": "Following", "followers": "Followers", "following": "Following!", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index afce03a4..7849aa20 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -17,7 +17,9 @@ }, "general": { "apply": "てきよう", - "submit": "そうしん" + "submit": "そうしん", + "more": "つづき", + "generic_error": "エラーになりました" }, "login": { "login": "ログイン", @@ -26,7 +28,8 @@ "password": "パスワード", "placeholder": "れい: lain", "register": "はじめる", - "username": "ユーザーめい" + "username": "ユーザーめい", + "hint": "はなしあいにくわわるには、ログインしてください" }, "nav": { "about": "これはなに?", @@ -49,7 +52,8 @@ "load_older": "ふるいつうちをみる", "notifications": "つうち", "read": "よんだ!", - "repeated_you": "あなたのステータスがリピートされました" + "repeated_you": "あなたのステータスがリピートされました", + "no_more_notifications": "つうちはありません" }, "post_status": { "new_status": "とうこうする", @@ -117,6 +121,7 @@ "delete_account_description": "あなたのアカウントとメッセージが、きえます。", "delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。", "delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。", + "avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。", "export_theme": "セーブ", "filtering": "フィルタリング", "filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。", @@ -132,8 +137,10 @@ "hide_attachments_in_tl": "タイムラインのファイルをかくす", "hide_isp": "インスタンススペシフィックパネルをかくす", "preload_images": "がぞうをさきよみする", + "use_one_click_nsfw": "NSFWなファイルを1クリックでひらく", "hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)", "hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)", + "hide_filtered_statuses": "フィルターされたとうこうをかくす", "import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする", "import_theme": "ロード", "inputRadius": "インプットフィールド", @@ -148,6 +155,8 @@ "lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる", "loop_video": "ビデオをくりかえす", "loop_video_silent_only": "おとのないビデオだけくりかえす", + "play_videos_in_modal": "ビデオをメディアビューアーでみる", + "use_contain_fit": "がぞうのサムネイルを、きりぬかない", "name": "なまえ", "name_bio": "なまえとプロフィール", "new_password": "あたらしいパスワード", @@ -157,8 +166,10 @@ "notification_visibility_mentions": "メンション", "notification_visibility_repeats": "リピート", "no_rich_text_description": "リッチテキストをつかわない", - "hide_follows_description": "フォローしている人を表示しない", - "hide_followers_description": "フォローしている人を表示しない", + "hide_follows_description": "フォローしているひとをみせない", + "hide_followers_description": "フォロワーをみせない", + "show_admin_badge": "アドミンのしるしをみる", + "show_moderator_badge": "モデレーターのしるしをみる", "nsfw_clickthrough": "NSFWなファイルをかくす", "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", @@ -185,6 +196,8 @@ "subject_line_email": "メールふう: \"re: サブジェクト\"", "subject_line_mastodon": "マストドンふう: そのままコピー", "subject_line_noop": "コピーしない", + "post_status_content_type": "とうこうのコンテントタイプ", + "status_content_type_plain": "プレーンテキスト", "stop_gifs": "カーソルをかさねたとき、GIFをうごかす", "streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする", "text": "もじ", @@ -318,13 +331,15 @@ "no_retweet_hint": "とうこうを「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります", "repeated": "リピート", "show_new": "よみこみ", - "up_to_date": "さいしん" + "up_to_date": "さいしん", + "no_more_statuses": "これでおわりです" }, "user_card": { "approve": "うけいれ", "block": "ブロック", "blocked": "ブロックしています!", "deny": "おことわり", + "favorites": "おきにいり", "follow": "フォロー", "follow_sent": "リクエストを、おくりました!", "follow_progress": "リクエストしています…", @@ -335,6 +350,7 @@ "following": "フォローしています!", "follows_you": "フォローされました!", "its_you": "これはあなたです!", + "media": "メディア", "mute": "ミュート", "muted": "ミュートしています!", "per_day": "/日", diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 56619455..46117fd7 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -296,7 +296,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot notifObj.image = action.attachments[0].url } - if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) { + if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { let notification = new window.Notification(title, notifObj) // Chrome is known for not closing notifications automatically // according to MDN, anyway. diff --git a/src/modules/users.js b/src/modules/users.js index 4d56ec6f..000cfd72 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -231,8 +231,14 @@ const users = { store.commit('setToken', result.access_token) store.dispatch('loginUser', result.access_token) } else { - let data = await response.json() - let errors = humanizeErrors(JSON.parse(data.error)) + const data = await response.json() + let errors = JSON.parse(data.error) + // replace ap_id with username + if (errors.ap_id) { + errors.username = errors.ap_id + delete errors.ap_id + } + errors = humanizeErrors(errors) store.commit('signUpFailure', errors) throw Error(errors) } diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 92daa04e..13d31d91 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -258,7 +258,7 @@ const fetchFriends = ({id, page, credentials}) => { } const exportFriends = ({id, credentials}) => { - let url = `${FRIENDS_URL}?user_id=${id}&export=true` + let url = `${FRIENDS_URL}?user_id=${id}&all=true` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 828c48f9..d20ce77f 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -117,6 +117,9 @@ export const parseUser = (data) => { output.statuses_count = data.statuses_count output.friends = [] output.followers = [] + if (data.pleroma) { + output.follow_request_count = data.pleroma.follow_request_count + } return output } |
