diff --git a/.eslintrc.js b/.eslintrc.js index 8e6549e5..800f9a4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { 'html' ], // add your custom rules here - 'rules': { + rules: { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await diff --git a/package.json b/package.json index 90bf48cf..03228133 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-lodash": "^3.2.11", "chromatism": "^3.0.0", + "cropperjs": "^1.4.3", "diff": "^3.0.1", "karma-mocha-reporter": "^2.2.1", "localforage": "^1.5.0", @@ -27,6 +28,7 @@ "sass-loader": "^4.0.2", "vue": "^2.5.13", "vue-chat-scroll": "^1.2.1", + "vue-compose": "^0.7.1", "vue-i18n": "^7.3.2", "vue-router": "^3.0.1", "vue-template-compiler": "^2.3.4", diff --git a/src/App.js b/src/App.js index 18bff2dd..214e0f48 100644 --- a/src/App.js +++ b/src/App.js @@ -66,12 +66,16 @@ export default { }) }, logo () { return this.$store.state.instance.logo }, - style () { + 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 }, @@ -82,7 +86,7 @@ export default { unseenNotificationsCount () { return this.unseenNotifications.length }, - showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel } + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel } }, methods: { scrollToTop () { diff --git a/src/App.scss b/src/App.scss index 93d7edb4..4bbc2d3c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,15 +1,21 @@ @import './_variables.scss'; #app { - background-size: cover; - background-attachment: fixed; - background-repeat: no-repeat; - background-position: 0 50px; min-height: 100vh; max-width: 100%; overflow: hidden; } +.app-bg-wrapper { + position: fixed; + z-index: -1; + height: 100%; + width: 100%; + background-size: cover; + background-repeat: no-repeat; + background-position: 0 50%; +} + i { user-select: none; } @@ -175,8 +181,7 @@ input, textarea, .select { color: $fallback--text; color: var(--text, $fallback--text); } - &:disabled, - { + &:disabled { &, & + label, & + label::before { @@ -643,10 +648,6 @@ nav { color: var(--lightText, $fallback--lightText); } - .text-format { - float: right; - } - div { padding-top: 5px; } @@ -719,3 +720,21 @@ nav { margin-right: 0.8em; } } + +.login-hint { + text-align: center; + + @media all and (min-width: 801px) { + display: none; + } + + a { + display: inline-block; + padding: 1em 0px; + width: 100%; + } +} + +.btn.btn-default { + min-height: 28px; +} diff --git a/src/App.vue b/src/App.vue index 082c6cb6..acbbeb75 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,6 @@ @@ -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; + } + } +} + diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js new file mode 100644 index 00000000..5dd0a9e5 --- /dev/null +++ b/src/components/mute_card/mute_card.js @@ -0,0 +1,37 @@ +import BasicUserCard from '../basic_user_card/basic_user_card.vue' + +const MuteCard = { + props: ['userId'], + data () { + return { + progress: false + } + }, + computed: { + user () { + return this.$store.getters.userById(this.userId) + }, + muted () { + return this.user.muted + } + }, + components: { + BasicUserCard + }, + methods: { + unmuteUser () { + this.progress = true + this.$store.dispatch('unmuteUser', this.user.id).then(() => { + this.progress = false + }) + }, + muteUser () { + this.progress = true + this.$store.dispatch('muteUser', this.user.id).then(() => { + this.progress = false + }) + } + } +} + +export default MuteCard diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue new file mode 100644 index 00000000..e1bfe20b --- /dev/null +++ b/src/components/mute_card/mute_card.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file 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 @@
  • - {{ $t("nav.friend_requests") }} + {{ $t("nav.friend_requests")}} +
  • @@ -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.js b/src/components/post_status_form/post_status_form.js index 5e8c2252..c28c51bf 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -56,6 +56,10 @@ const PostStatusForm = { ? this.copyMessageScope : this.$store.state.users.currentUser.default_scope + const contentType = typeof this.$store.state.config.postContentType === 'undefined' + ? this.$store.state.instance.postContentType + : this.$store.state.config.postContentType + return { dropFiles: [], submitDisabled: false, @@ -65,10 +69,10 @@ const PostStatusForm = { newStatus: { spoilerText: this.subject || '', status: statusText, - contentType: 'text/plain', nsfw: false, files: [], - visibility: scope + visibility: scope, + contentType }, 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 e09ad37f..5085570b 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/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js index 0db6efae..d45677e0 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.js +++ b/src/components/public_and_external_timeline/public_and_external_timeline.js @@ -7,7 +7,7 @@ const PublicAndExternalTimeline = { timeline () { return this.$store.state.statuses.timelines.publicAndExternal } }, created () { - this.$store.dispatch('startFetching', 'publicAndExternal') + this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' }) }, destroyed () { this.$store.dispatch('stopFetching', 'publicAndExternal') diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.vue b/src/components/public_and_external_timeline/public_and_external_timeline.vue index aded2ead..6be9f955 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.vue +++ b/src/components/public_and_external_timeline/public_and_external_timeline.vue @@ -1,5 +1,5 @@ diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js index 9b866be8..64c951ac 100644 --- a/src/components/public_timeline/public_timeline.js +++ b/src/components/public_timeline/public_timeline.js @@ -7,7 +7,7 @@ const PublicTimeline = { timeline () { return this.$store.state.statuses.timelines.public } }, created () { - this.$store.dispatch('startFetching', 'public') + this.$store.dispatch('startFetching', { timeline: 'public' }) }, destroyed () { this.$store.dispatch('stopFetching', 'public') diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 8d138485..23c1acdb 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -27,6 +27,11 @@ const settings = { : user.hideUserStats, hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats), + hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined' + ? instance.hideFilteredStatuses + : user.hideFilteredStatuses, + hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses), + notificationVisibilityLocal: user.notificationVisibility, replyVisibilityLocal: user.replyVisibility, loopVideoLocal: user.loopVideo, @@ -46,6 +51,11 @@ const settings = { : user.subjectLineBehavior, subjectLineBehaviorDefault: instance.subjectLineBehavior, + postContentTypeLocal: typeof user.postContentType === 'undefined' + ? instance.postContentType + : user.postContentType, + postContentTypeDefault: instance.postContentType, + alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined' ? instance.alwaysShowSubjectInput : user.alwaysShowSubjectInput, @@ -81,7 +91,8 @@ const settings = { }, currentSaveStateNotice () { return this.$store.state.interface.settings.currentSaveStateNotice - } + }, + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel } }, watch: { hideAttachmentsLocal (value) { @@ -96,6 +107,9 @@ const settings = { hideUserStatsLocal (value) { this.$store.dispatch('setOption', { name: 'hideUserStats', value }) }, + hideFilteredStatusesLocal (value) { + this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) + }, hideNsfwLocal (value) { this.$store.dispatch('setOption', { name: 'hideNsfw', value }) }, @@ -157,6 +171,9 @@ const settings = { subjectLineBehaviorLocal (value) { this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value }) }, + postContentTypeLocal (value) { + this.$store.dispatch('setOption', { name: 'postContentType', 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 9953780f..f5e00995 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -27,7 +27,7 @@
  • -
  • +
  • @@ -100,6 +100,28 @@ +
  • +
    + {{$t('settings.post_status_content_type')}} + +
    +
  • @@ -205,7 +227,6 @@ -
    {{$t('settings.replies_in_timeline')}} @@ -232,11 +253,18 @@
    -

    {{$t('settings.filtering_explanation')}}

    - +
    +

    {{$t('settings.filtering_explanation')}}

    + +
    +
    + + +
    - @@ -283,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 @@
  • {{ $t("nav.friend_requests") }} + +
  • diff --git a/src/components/status/status.js b/src/components/status/status.js index f295c24d..8407a432 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -10,8 +10,8 @@ import LinkPreview from '../link-preview/link-preview.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import fileType from 'src/services/file_type/file_type.service' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' -import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js' -import { filter, find } from 'lodash' +import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' +import { filter, find, unescape } from 'lodash' const Status = { name: 'Status', @@ -110,6 +110,14 @@ const Status = { return hits }, muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) }, + hideFilteredStatuses () { + return typeof this.$store.state.config.hideFilteredStatuses === 'undefined' + ? this.$store.state.instance.hideFilteredStatuses + : this.$store.state.config.hideFilteredStatuses + }, + hideStatus () { + return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) + }, isFocused () { // retweet or root of an expanded conversation if (this.focused) { @@ -201,14 +209,15 @@ const Status = { }, replySubject () { if (!this.status.summary) return '' + const decodedSummary = unescape(this.status.summary) const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined' ? this.$store.state.instance.subjectLineBehavior : this.$store.state.config.subjectLineBehavior - const startsWithRe = this.status.summary.match(/^re[: ]/i) + const startsWithRe = decodedSummary.match(/^re[: ]/i) if (behavior !== 'noop' && startsWithRe || behavior === 'masto') { - return this.status.summary + return decodedSummary } else if (behavior === 'email') { - return 're: '.concat(this.status.summary) + return 're: '.concat(decodedSummary) } else if (behavior === 'noop') { return '' } @@ -273,7 +282,7 @@ const Status = { } if (target.tagName === 'A') { if (target.className.match(/mention/)) { - const href = target.getAttribute('href') + const href = target.href const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) if (attn) { event.stopPropagation() @@ -283,6 +292,15 @@ const Status = { return } } + if (target.className.match(/hashtag/)) { + // Extract tag name from link url + const tag = extractTagFromUrl(target.href) + if (tag) { + const link = this.generateTagLink(tag) + this.$router.push(link) + return + } + } window.open(target.href, '_blank') } }, @@ -341,6 +359,9 @@ const Status = { generateUserProfileLink (id, name) { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) }, + generateTagLink (tag) { + return `/tag/${tag}` + }, setMedia () { const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments return () => this.$store.dispatch('setMedia', attachments) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 78963bc0..7d4e8338 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,5 @@