From 51a78e8b8a89065062d5b4c1b72d5bdd457d9663 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shp@cock.li>
Date: Thu, 25 Feb 2021 10:56:16 +0200
Subject: [PATCH] add a quick settings menu for timeline headers

---
 CHANGELOG.md                                  |   1 +
 .../moderation_tools/moderation_tools.vue     |  33 ++----
 src/components/popover/popover.js             |   7 ++
 src/components/popover/popover.vue            |  37 +++++-
 .../helpers/boolean_setting.vue               |  12 +-
 src/components/timeline/timeline.js           |   9 +-
 src/components/timeline/timeline.vue          |   4 +
 .../timeline/timeline_quick_settings.js       |  60 ++++++++++
 .../timeline/timeline_quick_settings.vue      | 105 ++++++++++++++++++
 src/i18n/en.json                              |   6 +
 10 files changed, 233 insertions(+), 41 deletions(-)
 create mode 100644 src/components/timeline/timeline_quick_settings.js
 create mode 100644 src/components/timeline/timeline_quick_settings.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fbb3575..ba0ad7c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Added
 - Added reason field for registration when approval is required
+- Added a quick settings to timeline header for easier access
 
 ## [2.2.3] - 2021-01-18
 ### Added
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index 5c7b82ec..bf25adc5 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -50,74 +50,74 @@
               class="button-default dropdown-item"
               @click="toggleTag(tags.FORCE_NSFW)"
             >
-              {{ $t('user_card.admin_menu.force_nsfw') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
               />
+              {{ $t('user_card.admin_menu.force_nsfw') }}
             </button>
             <button
               class="button-default dropdown-item"
               @click="toggleTag(tags.STRIP_MEDIA)"
             >
-              {{ $t('user_card.admin_menu.strip_media') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
               />
+              {{ $t('user_card.admin_menu.strip_media') }}
             </button>
             <button
               class="button-default dropdown-item"
               @click="toggleTag(tags.FORCE_UNLISTED)"
             >
-              {{ $t('user_card.admin_menu.force_unlisted') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
               />
+              {{ $t('user_card.admin_menu.force_unlisted') }}
             </button>
             <button
               class="button-default dropdown-item"
               @click="toggleTag(tags.SANDBOX)"
             >
-              {{ $t('user_card.admin_menu.sandbox') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
               />
+              {{ $t('user_card.admin_menu.sandbox') }}
             </button>
             <button
               v-if="user.is_local"
               class="button-default dropdown-item"
               @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
             >
-              {{ $t('user_card.admin_menu.disable_remote_subscription') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
               />
+              {{ $t('user_card.admin_menu.disable_remote_subscription') }}
             </button>
             <button
               v-if="user.is_local"
               class="button-default dropdown-item"
               @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
             >
-              {{ $t('user_card.admin_menu.disable_any_subscription') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
               />
+              {{ $t('user_card.admin_menu.disable_any_subscription') }}
             </button>
             <button
               v-if="user.is_local"
               class="button-default dropdown-item"
               @click="toggleTag(tags.QUARANTINE)"
             >
-              {{ $t('user_card.admin_menu.quarantine') }}
               <span
                 class="menu-checkbox"
                 :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
               />
+              {{ $t('user_card.admin_menu.quarantine') }}
             </button>
           </span>
         </div>
@@ -163,25 +163,6 @@
 <style lang="scss">
 @import '../../_variables.scss';
 
-.menu-checkbox {
-  float: right;
-  min-width: 22px;
-  max-width: 22px;
-  min-height: 22px;
-  max-height: 22px;
-  line-height: 22px;
-  text-align: center;
-  border-radius: 0px;
-  background-color: $fallback--fg;
-  background-color: var(--input, $fallback--fg);
-  box-shadow: 0px 0px 2px black inset;
-  box-shadow: var(--inputShadow);
-
-  &.menu-checkbox-checked::after {
-    content: '✓';
-  }
-}
-
 .moderation-tools-popover {
   height: 100%;
   .trigger {
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 5e417fa0..61cc728d 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -3,25 +3,32 @@ const Popover = {
   props: {
     // Action to trigger popover: either 'hover' or 'click'
     trigger: String,
+
     // Either 'top' or 'bottom'
     placement: String,
+
     // Takes object with properties 'x' and 'y', values of these can be
     // 'container' for using offsetParent as boundaries for either axis
     // or 'viewport'
     boundTo: Object,
+
     // Takes a selector to use as a replacement for the parent container
     // for getting boundaries for x an y axis
     boundToSelector: String,
+
     // Takes a top/bottom/left/right object, how much space to leave
     // between boundary and popover element
     margin: Object,
+
     // Takes a x/y object and tells how many pixels to offset from
     // anchor point on either axis
     offset: Object,
+
     // Replaces the classes you may want for the popover container.
     // Use 'popover-default' in addition to get the default popover
     // styles with your custom class.
     popoverClass: String,
+
     // If true, subtract padding when calculating position for the popover,
     // use it when popover offset looks to be different on top vs bottom.
     removePadding: Boolean
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 876dd61d..2e78a09e 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -6,8 +6,8 @@
     <button
       ref="trigger"
       class="button-unstyled -fullwidth popover-trigger-button"
-      @click="onClick"
       type="button"
+      @click="onClick"
     >
       <slot name="trigger" />
     </button>
@@ -82,10 +82,9 @@
 
   .dropdown-item {
     line-height: 21px;
-    margin-right: 5px;
     overflow: auto;
     display: block;
-    padding: .25rem 1.0rem .25rem 1.5rem;
+    padding: .5em 0.75em;
     clear: both;
     font-weight: 400;
     text-align: inherit;
@@ -101,10 +100,9 @@
     --btnText: var(--popoverText, $fallback--text);
 
     &-icon {
-      padding-left: 0.5rem;
-
       svg {
-        margin-right: 0.25rem;
+        width: 22px;
+        margin-right: 0.75rem;
         color: var(--menuPopoverIcon, $fallback--icon)
       }
     }
@@ -123,6 +121,33 @@
       }
     }
 
+    .menu-checkbox {
+      display: inline-block;
+      vertical-align: middle;
+      min-width: 22px;
+      max-width: 22px;
+      min-height: 22px;
+      max-height: 22px;
+      line-height: 22px;
+      text-align: center;
+      border-radius: 0px;
+      background-color: $fallback--fg;
+      background-color: var(--input, $fallback--fg);
+      box-shadow: 0px 0px 2px black inset;
+      box-shadow: var(--inputShadow);
+      margin-right: 0.75em;
+
+      &.menu-checkbox-checked::after {
+        font-size: 1.25em;
+        content: '✓';
+      }
+
+      &.menu-checkbox-radio::after {
+        font-size: 2em;
+        content: '•';
+      }
+    }
+
   }
 }
 </style>
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index b600b63b..146ad6c1 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -4,13 +4,13 @@
   >
     <Checkbox
       :checked="state"
-      @change="update"
       :disabled="disabled"
+      @change="update"
     >
       <span
         v-if="!!$slots.default"
         class="label"
-        >
+      >
         <slot />
       </span>
       <ModifiedIndicator :changed="isChanged" />
@@ -23,14 +23,14 @@ import { get, set } from 'lodash'
 import Checkbox from 'src/components/checkbox/checkbox.vue'
 import ModifiedIndicator from './modified_indicator.vue'
 export default {
-  props: [
-    'path',
-    'disabled'
-  ],
   components: {
     Checkbox,
     ModifiedIndicator
   },
+  props: [
+    'path',
+    'disabled'
+  ],
   computed: {
     pathDefault () {
       const [firstSegment, ...rest] = this.path.split('.')
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 665d195e..44f749c3 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -2,12 +2,14 @@ import Status from '../status/status.vue'
 import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
 import Conversation from '../conversation/conversation.vue'
 import TimelineMenu from '../timeline_menu/timeline_menu.vue'
+import TimelineQuickSettings from './timeline_quick_settings.vue'
 import { debounce, throttle, keyBy } from 'lodash'
 import { library } from '@fortawesome/fontawesome-svg-core'
-import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
+import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
-  faCircleNotch
+  faCircleNotch,
+  faCog
 )
 
 export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
@@ -47,7 +49,8 @@ const Timeline = {
   components: {
     Status,
     Conversation,
-    TimelineMenu
+    TimelineMenu,
+    TimelineQuickSettings
   },
   computed: {
     newStatusCount () {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 4c43fe5c..286477c2 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -16,6 +16,7 @@
       >
         {{ $t('timeline.up_to_date') }}
       </div>
+      <TimelineQuickSettings v-if="!embedded" />
     </div>
     <div :class="classes.body">
       <div
@@ -103,9 +104,12 @@
   max-width: 100%;
   flex-wrap: nowrap;
   align-items: center;
+  position: relative;
+
   .loadmore-button {
     flex-shrink: 0;
   }
+
   .loadmore-text {
     flex-shrink: 0;
     line-height: 1em;
diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js
new file mode 100644
index 00000000..eb1eb8fb
--- /dev/null
+++ b/src/components/timeline/timeline_quick_settings.js
@@ -0,0 +1,60 @@
+import Popover from '../popover/popover.vue'
+import BooleanSetting from '../settings_modal/helpers/boolean_setting.vue'
+import { mapGetters } from 'vuex'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faFilter, faWrench } from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+  faFilter,
+  faWrench
+)
+
+const TimelineQuickSettings = {
+  components: {
+    Popover,
+    BooleanSetting
+  },
+  methods: {
+    setReplyVisibility (visibility) {
+      console.log('set reply visibility', visibility)
+      this.$store.dispatch('setOption', { name: 'replyVisibility', value: visibility })
+      this.$store.dispatch('queueFlushAll')
+    },
+    openTab (tab) {
+      this.$store.dispatch('openSettingsModalTab', tab)
+    }
+  },
+  computed: {
+    ...mapGetters(['mergedConfig']),
+    replyVisibilitySelf: {
+      get () { return this.mergedConfig.replyVisibility === 'self' },
+      set () { this.setReplyVisibility('self') }
+    },
+    replyVisibilityFollowing: {
+      get () { return this.mergedConfig.replyVisibility === 'following' },
+      set () { this.setReplyVisibility('following') }
+    },
+    replyVisibilityAll: {
+      get () { return this.mergedConfig.replyVisibility === 'all' },
+      set () { this.setReplyVisibility('all') }
+    },
+    hideMedia: {
+      get () { return this.mergedConfig.hideAttachments || this.mergedConfig.hideAttachmentsInConv },
+      set () {
+        const value = !this.hideMedia
+        this.$store.dispatch('setOption', { name: 'hideAttachments', value })
+        this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
+      }
+    },
+    hideMutedPosts: {
+      get () { return this.mergedConfig.hideMutedPosts || this.mergedConfig.hideFilteredStatuses },
+      set () {
+        const value = !this.hideMutedPosts
+        this.$store.dispatch('setOption', { name: 'hideMutedPosts', value })
+        this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
+      }
+    }
+  }
+}
+
+export default TimelineQuickSettings
diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue
new file mode 100644
index 00000000..51edff71
--- /dev/null
+++ b/src/components/timeline/timeline_quick_settings.vue
@@ -0,0 +1,105 @@
+<template>
+  <Popover
+    trigger="click"
+    class="TimelineQuickSettings"
+    :bound-to="{ x: 'container' }"
+  >
+    <div
+      slot="content"
+      class="timeline-settings-menu dropdown-menu"
+    >
+      <button
+        class="button-default dropdown-item"
+        @click="replyVisibilityAll = true"
+      >
+        <span
+          class="menu-checkbox"
+          :class="{ 'menu-checkbox-radio': replyVisibilityAll }"
+        />{{ $t('settings.reply_visibility_all') }}
+      </button>
+      <button
+        class="button-default dropdown-item"
+        @click="replyVisibilityFollowing = true"
+      >
+        <span
+          class="menu-checkbox"
+          :class="{ 'menu-checkbox-radio': replyVisibilityFollowing }"
+        />{{ $t('settings.reply_visibility_following_short') }}
+      </button>
+      <button
+        class="button-default dropdown-item"
+        @click="replyVisibilitySelf = true"
+      >
+        <span
+          class="menu-checkbox"
+          :class="{ 'menu-checkbox-radio': replyVisibilitySelf }"
+        />{{ $t('settings.reply_visibility_self_short') }}
+      </button>
+      <div
+        role="separator"
+        class="dropdown-divider"
+      />
+      <button
+        class="button-default dropdown-item"
+        @click="hideMedia = !hideMedia"
+      >
+        <span
+          class="menu-checkbox"
+          :class="{ 'menu-checkbox-checked': hideMedia }"
+        />{{ $t('settings.hide_media_previews') }}
+      </button>
+      <button
+        class="button-default dropdown-item"
+        @click="hideMutedPosts = !hideMutedPosts"
+      >
+        <span
+          class="menu-checkbox"
+          :class="{ 'menu-checkbox-checked': hideMutedPosts }"
+        />{{ $t('settings.hide_all_muted_posts') }}
+      </button>
+      <button
+        class="button-default dropdown-item dropdown-item-icon"
+        @click="openTab('filtering')"
+      >
+        <FAIcon icon="filter" />{{ $t('settings.word_filter') }}
+      </button>
+      <button
+        class="button-default dropdown-item dropdown-item-icon"
+        @click="openTab('general')"
+      >
+        <FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
+      </button>
+    </div>
+    <div slot="trigger">
+      <FAIcon icon="cog" />
+    </div>
+  </Popover>
+</template>
+
+<script src="./timeline_quick_settings.js"></script>
+
+<style lang="scss">
+
+.TimelineQuickSettings {
+  align-self: stretch;
+
+  > button {
+    font-size: 1.2em;
+    padding-left: 0.7em;
+    padding-right: 0.2em;
+    line-height: 100%;
+    height: 100%;
+  }
+
+  .dropdown-item {
+    margin: 0;
+  }
+
+  .timeline-settings-menu {
+    display: flex;
+    min-width: 12em;
+    flex-direction: column;
+  }
+}
+
+</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0e069785..ec16f044 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -325,6 +325,7 @@
     "export_theme": "Save preset",
     "filtering": "Filtering",
     "filtering_explanation": "All statuses containing these words will be muted, one per line",
+    "word_filter": "Word filter",
     "follow_export": "Follow export",
     "follow_export_button": "Export your follows to a csv file",
     "follow_import": "Follow import",
@@ -335,7 +336,9 @@
     "general": "General",
     "hide_attachments_in_convo": "Hide attachments in conversations",
     "hide_attachments_in_tl": "Hide attachments in timeline",
+    "hide_media_previews": "Hide media previews",
     "hide_muted_posts": "Hide posts of muted users",
+    "hide_all_muted_posts": "Hide muted posts",
     "max_thumbnails": "Maximum amount of thumbnails per post",
     "hide_isp": "Hide instance-specific panel",
     "hide_wallpaper": "Hide instance wallpaper",
@@ -405,6 +408,8 @@
     "reply_visibility_all": "Show all replies",
     "reply_visibility_following": "Only show replies directed at me or users I'm following",
     "reply_visibility_self": "Only show replies directed at me",
+    "reply_visibility_following_short": "Show replies to my follows",
+    "reply_visibility_self_short": "Show replies to self",
     "autohide_floating_post_button": "Automatically hide New Post button (mobile)",
     "saving_err": "Error saving settings",
     "saving_ok": "Settings saved",
@@ -458,6 +463,7 @@
     "notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
     "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
     "enable_web_push_notifications": "Enable web push notifications",
+    "more_settings": "More settings",
     "style": {
       "switcher": {
         "keep_color": "Keep colors",