From e067783a30be41644abe1aecca631ef31cf476a8 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 17 Mar 2022 12:59:10 -0400
Subject: Add announcement display with placeholder messages

---
 src/components/announcement/announcement.vue | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 src/components/announcement/announcement.vue

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
new file mode 100644
index 00000000..c31328ee
--- /dev/null
+++ b/src/components/announcement/announcement.vue
@@ -0,0 +1,19 @@
+<template>
+  <div class="announcement">
+    <rich-content :html="content" />
+  </div>
+</template>
+
+<script src="./announcement.js"></script>
+
+<style lang="scss">
+@import "../../variables";
+
+.announcement {
+  border-bottom-width: 1px;
+  border-bottom-style: solid;
+  border-bottom-color: var(--border, $fallback--border);
+  border-radius: 0;
+  padding: var(--status-margin, $status-margin);
+}
+</style>
-- 
cgit v1.2.3-70-g09d2


From 95750509b14ab5f5a5d8c9a5defafd95ee7efbe3 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 17 Mar 2022 14:01:45 -0400
Subject: Fetch real data from backend

---
 src/components/announcement/announcement.js        | 10 ++++
 src/components/announcement/announcement.vue       | 24 ++++++++-
 .../announcements_page/announcements_page.js       | 33 ++----------
 src/main.js                                        |  4 +-
 src/modules/announcements.js                       | 60 ++++++++++++++++++++++
 src/services/api/api.service.js                    | 18 ++++++-
 6 files changed, 117 insertions(+), 32 deletions(-)
 create mode 100644 src/modules/announcements.js

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 595f3b4e..9e043e48 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -6,6 +6,16 @@ const Announcement = {
   computed: {
     content () {
       return this.announcement.content
+    },
+    isRead () {
+      return this.announcement.read
+    }
+  },
+  methods: {
+    markAsRead () {
+      if (!this.isRead) {
+        return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
+      }
     }
   }
 }
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index c31328ee..d8591468 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -1,6 +1,24 @@
 <template>
   <div class="announcement">
-    <rich-content :html="content" />
+    <div class="heading">
+      <h4>{{ $t('announcements.title') }}</h4>
+    </div>
+    <div class="body">
+      <rich-content
+        :html="content"
+        :emoji="announcement.emojis"
+        :handle-links="true"
+      />
+    </div>
+    <div class="footer">
+      <button
+        class="btn button-default"
+        :class="{ toggled: isRead }"
+        @click="markAsRead"
+      >
+        {{ $t('announcements.mark_as_read_action') }}
+      </button>
+    </div>
   </div>
 </template>
 
@@ -15,5 +33,9 @@
   border-bottom-color: var(--border, $fallback--border);
   border-radius: 0;
   padding: var(--status-margin, $status-margin);
+
+  .heading, .body {
+    margin-bottom: var(--status-margin, $status-margin);
+  }
 }
 </style>
diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js
index 716468e6..a2a218fd 100644
--- a/src/components/announcements_page/announcements_page.js
+++ b/src/components/announcements_page/announcements_page.js
@@ -4,37 +4,12 @@ const AnnouncementsPage = {
   components: {
     Announcement
   },
+  mounted () {
+    this.$store.dispatch('fetchAnnouncements')
+  },
   computed: {
     announcements () {
-      return [{
-        "id": "8",
-        "content": "<p>Looks like there was an issue processing audio attachments without embedded art since yesterday due to an experimental new feature. That issue has now been fixed, so you may see older posts with audio from other servers pop up in your feeds now as they are being finally properly processed. Sorry!</p>",
-        "starts_at": null,
-        "ends_at": null,
-        "all_day": false,
-        "published_at": "2020-07-03T01:27:38.726Z",
-        "updated_at": "2020-07-03T01:27:38.752Z",
-        "read": true,
-        "mentions": [],
-        "statuses": [],
-        "tags": [],
-        "emojis": [],
-        "reactions": []
-      }, {
-        "id": "8",
-        "content": "<p>Looks like there was an issue processing audio attachments without embedded art since yesterday due to an experimental new feature. That issue has now been fixed, so you may see older posts with audio from other servers pop up in your feeds now as they are being finally properly processed. Sorry!</p>",
-        "starts_at": null,
-        "ends_at": null,
-        "all_day": false,
-        "published_at": "2020-07-03T01:27:38.726Z",
-        "updated_at": "2020-07-03T01:27:38.752Z",
-        "read": true,
-        "mentions": [],
-        "statuses": [],
-        "tags": [],
-        "emojis": [],
-        "reactions": []
-      }]
+      return this.$store.state.announcements.announcements
     }
   }
 }
diff --git a/src/main.js b/src/main.js
index 6aa9cbb7..d3e60a0f 100644
--- a/src/main.js
+++ b/src/main.js
@@ -24,6 +24,7 @@ import editStatusModule from './modules/editStatus.js'
 import statusHistoryModule from './modules/statusHistory.js'
 
 import chatsModule from './modules/chats.js'
+import announcementsModule from './modules/announcements.js'
 
 import { createI18n } from 'vue-i18n'
 
@@ -91,7 +92,8 @@ const persistedStateOptions = {
       postStatus: postStatusModule,
       editStatus: editStatusModule,
       statusHistory: statusHistoryModule,
-      chats: chatsModule
+      chats: chatsModule,
+      announcements: announcementsModule
     },
     plugins,
     strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
new file mode 100644
index 00000000..2ab2600c
--- /dev/null
+++ b/src/modules/announcements.js
@@ -0,0 +1,60 @@
+import { set } from 'vue'
+
+const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5
+
+export const defaultState = {
+  announcements: [],
+  fetchAnnouncementsTimer: undefined
+}
+
+export const mutations = {
+  setAnnouncements (state, announcements) {
+    set(state, 'announcements', announcements)
+  },
+  setAnnouncementRead (state, { id, read }) {
+    if (!state.announcements[id]) {
+      return
+    }
+
+    set(state.announcements[id], 'read', read)
+  },
+  setFetchAnnouncementsTimer (state, timer) {
+    set(state, 'fetchAnnouncementsTimer', announcements)
+  }
+}
+
+const announcements = {
+  state: defaultState,
+  mutations,
+  actions: {
+    fetchAnnouncements (store) {
+      return store.rootState.api.backendInteractor.fetchAnnouncements()
+        .then(announcements => {
+          store.commit('setAnnouncements', announcements)
+        })
+    },
+    markAnnouncementAsRead (store, id) {
+      return store.rootState.api.backendInteractor.dismissAnnouncement({ id })
+        .then(() => {
+          store.commit('setAnnouncementRead', { id, read: true })
+        })
+    },
+    startFetchingAnnouncements (store) {
+      if (store.state.fetchAnnouncementsTimer) {
+        return
+      }
+
+      const interval = setInterval(() => store.dispatch('fetchAnnouncements'), FETCH_ANNOUNCEMENT_INTERVAL_MS)
+      store.commit('setFetchAnnouncementsTimer', interval)
+
+      return store.dispatch('fetchAnnouncements')
+    },
+    stopFetchingAnnouncements (store) {
+      const interval = store.state.fetchAnnouncementsTimer
+      store.commit('setFetchAnnouncementsTimer', undefined)
+      clearInterval(interval)
+    }
+  }
+}
+
+export default announcements
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 1ec77b37..10d43632 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -90,6 +90,8 @@ const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_LISTS_URL = '/api/v1/lists'
 const MASTODON_STREAMING = '/api/v1/streaming'
 const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
+const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
+const MASTODON_ANNOUNCEMENTS_DISMISS_URL = id => `/api/v1/announcements/${id}/dismiss`
 const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
 const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
 const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
@@ -1361,6 +1363,18 @@ const dismissNotification = ({ credentials, id }) => {
   })
 }
 
+const fetchAnnouncements = ({ credentials }) => {
+  return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
+}
+
+const dismissAnnouncement = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
+    credentials,
+    method: 'POST'
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1687,7 +1701,9 @@ const apiService = {
   readChat,
   deleteChatMessage,
   setReportState,
-  fetchUserInLists
+  fetchUserInLists,
+  fetchAnnouncements,
+  dismissAnnouncement
 }
 
 export default apiService
-- 
cgit v1.2.3-70-g09d2


From e877fedb0e60e795bd5c6aa3e706c64f12746c66 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 17 Mar 2022 15:07:04 -0400
Subject: Implement deleting announcement

---
 src/components/announcement/announcement.js  |  7 +++++++
 src/components/announcement/announcement.vue | 18 ++++++++++++++++++
 src/modules/announcements.js                 |  6 ++++++
 src/services/api/api.service.js              | 12 +++++++++++-
 4 files changed, 42 insertions(+), 1 deletion(-)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 9e043e48..b1f5ee77 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -1,9 +1,13 @@
+import { mapState } from 'vuex'
 
 const Announcement = {
   props: {
     announcement: Object
   },
   computed: {
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    }),
     content () {
       return this.announcement.content
     },
@@ -16,6 +20,9 @@ const Announcement = {
       if (!this.isRead) {
         return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
       }
+    },
+    deleteAnnouncement () {
+      return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
     }
   }
 }
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index d8591468..d6f35b9f 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -12,12 +12,20 @@
     </div>
     <div class="footer">
       <button
+        v-if="currentUser"
         class="btn button-default"
         :class="{ toggled: isRead }"
         @click="markAsRead"
       >
         {{ $t('announcements.mark_as_read_action') }}
       </button>
+      <button
+        v-if="currentUser && currentUser.role === 'admin'"
+        class="btn button-default"
+        @click="deleteAnnouncement"
+      >
+        {{ $t('announcements.delete_action') }}
+      </button>
     </div>
   </div>
 </template>
@@ -37,5 +45,15 @@
   .heading, .body {
     margin-bottom: var(--status-margin, $status-margin);
   }
+
+  .footer {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-around;
+
+    .btn {
+      min-width: 10em;
+    }
+  }
 }
 </style>
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
index 76cea154..ddc51c33 100644
--- a/src/modules/announcements.js
+++ b/src/modules/announcements.js
@@ -59,6 +59,12 @@ const announcements = {
         .then(() => {
           return store.dispatch('fetchAnnouncements')
         })
+    },
+    deleteAnnouncement (store, id) {
+      return store.rootState.api.backendInteractor.deleteAnnouncement({ id })
+        .then(() => {
+          return store.dispatch('fetchAnnouncements')
+        })
     }
   }
 }
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 5206602d..96b8bd70 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -103,6 +103,7 @@ const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/
 const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
 const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
 const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
+const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
 
 const oldfetch = window.fetch
 
@@ -1399,6 +1400,14 @@ const postAnnouncement = ({ credentials, content, startsAt, endsAt, allDay }) =>
   })
 }
 
+const deleteAnnouncement = ({ id, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_DELETE_ANNOUNCEMENT_URL(id),
+    credentials,
+    method: 'DELETE'
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1728,7 +1737,8 @@ const apiService = {
   fetchUserInLists,
   fetchAnnouncements,
   dismissAnnouncement,
-  postAnnouncement
+  postAnnouncement,
+  deleteAnnouncement
 }
 
 export default apiService
-- 
cgit v1.2.3-70-g09d2


From 04fbb6d93d6217812dc1b594b65f237bcee692a7 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 17 Mar 2022 16:51:32 -0400
Subject: Show announcement dates

---
 src/components/announcement/announcement.js        | 35 ++++++++++++++
 src/components/announcement/announcement.vue       | 54 ++++++++++++++++------
 .../announcement_editor/announcement_editor.js     | 13 ++++++
 .../announcement_editor/announcement_editor.vue    | 52 +++++++++++++++++++++
 .../announcements_page/announcements_page.js       |  4 +-
 .../announcements_page/announcements_page.vue      | 41 +---------------
 src/modules/announcements.js                       |  7 ++-
 src/services/api/api.service.js                    |  8 +++-
 8 files changed, 156 insertions(+), 58 deletions(-)
 create mode 100644 src/components/announcement_editor/announcement_editor.js
 create mode 100644 src/components/announcement_editor/announcement_editor.vue

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index b1f5ee77..309eecea 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -1,6 +1,21 @@
 import { mapState } from 'vuex'
+import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
+import localeService from '../../services/locale/locale.service.js'
 
 const Announcement = {
+  components: {
+    AnnouncementEditor
+  },
+  data () {
+    return {
+      editing: false,
+      newAnnouncement: {
+        content: '',
+        startsAt: undefined,
+        endsAt: undefined
+      }
+    }
+  },
   props: {
     announcement: Object
   },
@@ -13,6 +28,22 @@ const Announcement = {
     },
     isRead () {
       return this.announcement.read
+    },
+    startsAt () {
+      const time = this.announcement['starts_at']
+      if (!time) {
+        return
+      }
+
+      return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+    },
+    endsAt () {
+      const time = this.announcement['ends_at']
+      if (!time) {
+        return
+      }
+
+      return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
     }
   },
   methods: {
@@ -23,6 +54,10 @@ const Announcement = {
     },
     deleteAnnouncement () {
       return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
+    },
+    formatTimeOrDate (time, locale) {
+      const d = new Date(time)
+      return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
     }
   }
 }
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index d6f35b9f..5ddeebf5 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -5,27 +5,42 @@
     </div>
     <div class="body">
       <rich-content
+        v-if="!editing"
         :html="content"
         :emoji="announcement.emojis"
         :handle-links="true"
       />
+      <announcement-editor
+        v-else
+        :announcement="newAnnouncement"
+      />
     </div>
     <div class="footer">
-      <button
-        v-if="currentUser"
-        class="btn button-default"
-        :class="{ toggled: isRead }"
-        @click="markAsRead"
-      >
-        {{ $t('announcements.mark_as_read_action') }}
-      </button>
-      <button
-        v-if="currentUser && currentUser.role === 'admin'"
-        class="btn button-default"
-        @click="deleteAnnouncement"
-      >
-        {{ $t('announcements.delete_action') }}
-      </button>
+      <div class="times">
+        <span v-if="startsAt">
+          {{ $t('announcements.start_time_display', { time: startsAt }) }}
+        </span>
+        <span v-if="endsAt">
+          {{ $t('announcements.end_time_display', { time: endsAt }) }}
+        </span>
+      </div>
+      <div class="actions">
+        <button
+          v-if="currentUser"
+          class="btn button-default"
+          :class="{ toggled: isRead }"
+          @click="markAsRead"
+        >
+          {{ $t('announcements.mark_as_read_action') }}
+        </button>
+        <button
+          v-if="currentUser && currentUser.role === 'admin'"
+          class="btn button-default"
+          @click="deleteAnnouncement"
+        >
+          {{ $t('announcements.delete_action') }}
+        </button>
+      </div>
     </div>
   </div>
 </template>
@@ -47,6 +62,15 @@
   }
 
   .footer {
+    display: flex;
+    flex-direction: column;
+    .times {
+      display: flex;
+      flex-direction: column;
+    }
+  }
+
+  .footer .actions {
     display: flex;
     flex-direction: row;
     justify-content: space-around;
diff --git a/src/components/announcement_editor/announcement_editor.js b/src/components/announcement_editor/announcement_editor.js
new file mode 100644
index 00000000..79a03afe
--- /dev/null
+++ b/src/components/announcement_editor/announcement_editor.js
@@ -0,0 +1,13 @@
+import Checkbox from '../checkbox/checkbox.vue'
+
+const AnnouncementEditor = {
+  components: {
+    Checkbox
+  },
+  props: {
+    announcement: Object,
+    disabled: Boolean
+  }
+}
+
+export default AnnouncementEditor
diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue
new file mode 100644
index 00000000..e2418b8d
--- /dev/null
+++ b/src/components/announcement_editor/announcement_editor.vue
@@ -0,0 +1,52 @@
+<template>
+  <div class="announcement-editor">
+    <textarea
+      ref="textarea"
+      v-model="announcement.content"
+      class="post-textarea"
+      rows="1"
+      cols="1"
+      :placeholder="$t('announcements.post_placeholder')"
+      :disabled="disabled"
+    />
+    <span class="announcement-metadata">
+      <label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
+      <input
+        id="announcement-start-time"
+        v-model="announcement.startsAt"
+        :type="announcement.allDay ? 'date' : 'datetime-local'"
+        :disabled="disabled"
+      >
+    </span>
+    <span class="announcement-metadata">
+      <label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
+      <input
+        id="announcement-end-time"
+        v-model="announcement.endsAt"
+        :type="announcement.allDay ? 'date' : 'datetime-local'"
+        :disabled="disabled"
+      >
+    </span>
+    <span class="announcement-metadata">
+      <Checkbox
+        id="announcement-all-day"
+        v-model="announcement.allDay"
+        :disabled="disabled"
+      />
+      <label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
+    </span>
+  </div>
+</template>
+
+<script src="./announcement_editor.js"></script>
+
+<style lang="scss">
+    .announcement-editor {
+      display: flex;
+      align-items: stretch;
+      flex-direction: column;
+      .announcement-metadata {
+        margin-top: 0.5em;
+      }
+    }
+</style>
diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js
index 30759533..0bb4892e 100644
--- a/src/components/announcements_page/announcements_page.js
+++ b/src/components/announcements_page/announcements_page.js
@@ -1,11 +1,11 @@
 import { mapState } from 'vuex'
 import Announcement from '../announcement/announcement.vue'
-import Checkbox from '../checkbox/checkbox.vue'
+import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
 
 const AnnouncementsPage = {
   components: {
     Announcement,
-    Checkbox
+    AnnouncementEditor
   },
   data () {
     return {
diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue
index e81edee1..54307c4d 100644
--- a/src/components/announcements_page/announcements_page.vue
+++ b/src/components/announcements_page/announcements_page.vue
@@ -14,38 +14,10 @@
             <h4>{{ $t('announcements.post_form_header') }}</h4>
           </div>
           <div class="body">
-            <textarea
-              ref="textarea"
-              v-model="newAnnouncement.content"
-              class="post-textarea"
-              rows="1"
-              cols="1"
-              :placeholder="$t('announcements.post_placeholder')"
+            <announcement-editor
+              :announcement="newAnnouncement"
               :disabled="posting"
             />
-            <span class="announcement-metadata">
-              <label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
-              <input
-                id="announcement-start-time"
-                v-model="newAnnouncement.startsAt"
-                :type="newAnnouncement.allDay ? 'date' : 'datetime-local'"
-              >
-            </span>
-            <span class="announcement-metadata">
-              <label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
-              <input
-                id="announcement-end-time"
-                v-model="newAnnouncement.endsAt"
-                :type="newAnnouncement.allDay ? 'date' : 'datetime-local'"
-              >
-            </span>
-            <span class="announcement-metadata">
-              <Checkbox
-                id="announcement-all-day"
-                v-model="newAnnouncement.allDay"
-              />
-              <label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
-            </span>
           </div>
           <div class="footer">
             <button
@@ -99,15 +71,6 @@
       margin-bottom: var(--status-margin, $status-margin);
     }
 
-    .body {
-      display: flex;
-      align-items: stretch;
-      flex-direction: column;
-      .announcement-metadata {
-        margin-top: 0.5em;
-      }
-    }
-
     .post-textarea {
       resize: vertical;
       height: 10em;
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
index ddc51c33..a42acf93 100644
--- a/src/modules/announcements.js
+++ b/src/modules/announcements.js
@@ -28,7 +28,12 @@ const announcements = {
   mutations,
   actions: {
     fetchAnnouncements (store) {
-      return store.rootState.api.backendInteractor.fetchAnnouncements()
+      const currentUser = store.rootState.users.currentUser
+      const isAdmin = currentUser && currentUser.role === 'admin'
+
+      return (isAdmin
+        ? store.rootState.api.backendInteractor.adminFetchAnnouncements()
+        : store.rootState.api.backendInteractor.fetchAnnouncements())
         .then(announcements => {
           store.commit('setAnnouncements', announcements)
         })
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 752d5811..c2bfe8cc 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -102,6 +102,7 @@ const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
 const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
 const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
 const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
+const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
 const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
 const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
 
@@ -1365,6 +1366,10 @@ const dismissNotification = ({ credentials, id }) => {
   })
 }
 
+const adminFetchAnnouncements = ({ credentials }) => {
+  return promisedRequest({ url: PLEROMA_ANNOUNCEMENTS_URL, credentials })
+}
+
 const fetchAnnouncements = ({ credentials }) => {
   return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
 }
@@ -1738,7 +1743,8 @@ const apiService = {
   fetchAnnouncements,
   dismissAnnouncement,
   postAnnouncement,
-  deleteAnnouncement
+  deleteAnnouncement,
+  adminFetchAnnouncements
 }
 
 export default apiService
-- 
cgit v1.2.3-70-g09d2


From 89c49b6fb46e607aee07afd5bc436e8f2f605bfe Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 17 Mar 2022 17:11:53 -0400
Subject: Allow editing announcements

---
 src/components/announcement/announcement.js        | 31 +++++++++++++-
 src/components/announcement/announcement.vue       | 50 +++++++++++++++++++++-
 .../announcement_editor/announcement_editor.vue    | 24 +++++++----
 .../announcements_page/announcements_page.vue      |  7 ---
 src/modules/announcements.js                       |  6 +++
 src/services/api/api.service.js                    | 23 ++++++++--
 6 files changed, 118 insertions(+), 23 deletions(-)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 309eecea..253e2bb2 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -12,8 +12,10 @@ const Announcement = {
       newAnnouncement: {
         content: '',
         startsAt: undefined,
-        endsAt: undefined
-      }
+        endsAt: undefined,
+        allDay: undefined
+      },
+      editError: ''
     }
   },
   props: {
@@ -58,6 +60,31 @@ const Announcement = {
     formatTimeOrDate (time, locale) {
       const d = new Date(time)
       return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
+    },
+    enterEditMode () {
+      this.newAnnouncement.content = this.announcement.pleroma['raw_content']
+      this.newAnnouncement.startsAt = this.announcement['starts_at']
+      this.newAnnouncement.endsAt = this.announcement['ends_at']
+      this.newAnnouncement.allDay = this.announcement['all_day']
+      this.editing = true
+    },
+    submitEdit () {
+      this.$store.dispatch('editAnnouncement', {
+        id: this.announcement.id,
+        ...this.newAnnouncement
+      })
+        .then(() => {
+          this.editing = false
+        })
+        .catch(error => {
+          this.editError = error.error
+        })
+    },
+    cancelEdit () {
+      this.editing = false
+    },
+    clearError () {
+      this.editError = undefined
     }
   }
 }
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index 5ddeebf5..1e4edcca 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -16,7 +16,10 @@
       />
     </div>
     <div class="footer">
-      <div class="times">
+      <div
+        v-if="!editing"
+        class="times"
+      >
         <span v-if="startsAt">
           {{ $t('announcements.start_time_display', { time: startsAt }) }}
         </span>
@@ -24,7 +27,10 @@
           {{ $t('announcements.end_time_display', { time: endsAt }) }}
         </span>
       </div>
-      <div class="actions">
+      <div
+        v-if="!editing"
+        class="actions"
+      >
         <button
           v-if="currentUser"
           class="btn button-default"
@@ -33,6 +39,13 @@
         >
           {{ $t('announcements.mark_as_read_action') }}
         </button>
+        <button
+          v-if="currentUser && currentUser.role === 'admin'"
+          class="btn button-default"
+          @click="enterEditMode"
+        >
+          {{ $t('announcements.edit_action') }}
+        </button>
         <button
           v-if="currentUser && currentUser.role === 'admin'"
           class="btn button-default"
@@ -41,6 +54,39 @@
           {{ $t('announcements.delete_action') }}
         </button>
       </div>
+      <div
+        v-else
+        class="actions"
+      >
+        <button
+          class="btn button-default"
+          @click="submitEdit"
+        >
+          {{ $t('announcements.submit_edit_action') }}
+        </button>
+        <button
+          class="btn button-default"
+          @click="cancelEdit"
+        >
+          {{ $t('announcements.cancel_edit_action') }}
+        </button>
+        <div
+          v-if="editing && editError"
+          class="alert error"
+        >
+          {{ $t('announcements.edit_error', { error }) }}
+          <button
+            class="button-unstyled"
+            @click="clearError"
+          >
+            <FAIcon
+              class="fa-scale-110 fa-old-padding"
+              icon="times"
+              :title="$t('announcements.close_error')"
+            />
+          </button>
+        </div>
+      </div>
     </div>
   </div>
 </template>
diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue
index e2418b8d..0f29f9f7 100644
--- a/src/components/announcement_editor/announcement_editor.vue
+++ b/src/components/announcement_editor/announcement_editor.vue
@@ -41,12 +41,20 @@
 <script src="./announcement_editor.js"></script>
 
 <style lang="scss">
-    .announcement-editor {
-      display: flex;
-      align-items: stretch;
-      flex-direction: column;
-      .announcement-metadata {
-        margin-top: 0.5em;
-      }
-    }
+.announcement-editor {
+  display: flex;
+  align-items: stretch;
+  flex-direction: column;
+
+  .announcement-metadata {
+    margin-top: 0.5em;
+  }
+
+  .post-textarea {
+    resize: vertical;
+    height: 10em;
+    overflow: none;
+    box-sizing: content-box;
+  }
+}
 </style>
diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue
index 54307c4d..b1489dec 100644
--- a/src/components/announcements_page/announcements_page.vue
+++ b/src/components/announcements_page/announcements_page.vue
@@ -71,13 +71,6 @@
       margin-bottom: var(--status-margin, $status-margin);
     }
 
-    .post-textarea {
-      resize: vertical;
-      height: 10em;
-      overflow: none;
-      box-sizing: content-box;
-    }
-
     .post-button {
       min-width: 10em;
     }
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
index a42acf93..1d97c67c 100644
--- a/src/modules/announcements.js
+++ b/src/modules/announcements.js
@@ -65,6 +65,12 @@ const announcements = {
           return store.dispatch('fetchAnnouncements')
         })
     },
+    editAnnouncement (store, { id, content, startsAt, endsAt, allDay }) {
+      return store.rootState.api.backendInteractor.editAnnouncement({ id, content, startsAt, endsAt, allDay })
+        .then(() => {
+          return store.dispatch('fetchAnnouncements')
+        })
+    },
     deleteAnnouncement (store, id) {
       return store.rootState.api.backendInteractor.deleteAnnouncement({ id })
         .then(() => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index c2bfe8cc..0d4c674a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -104,6 +104,7 @@ const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
 const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
 const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
 const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
+const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
 const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
 
 const oldfetch = window.fetch
@@ -1382,26 +1383,39 @@ const dismissAnnouncement = ({ id, credentials }) => {
   })
 }
 
-const postAnnouncement = ({ credentials, content, startsAt, endsAt, allDay }) => {
+const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
   const payload = { content }
 
   if (typeof startsAt !== 'undefined') {
-    payload['starts_at'] = new Date(startsAt).toISOString()
+    payload['starts_at'] = startsAt ? new Date(startsAt).toISOString() : null
   }
 
   if (typeof endsAt !== 'undefined') {
-    payload['ends_at'] = new Date(endsAt).toISOString()
+    payload['ends_at'] = endsAt ? new Date(endsAt).toISOString() : null
   }
 
   if (typeof allDay !== 'undefined') {
     payload['all_day'] = allDay
   }
 
+  return payload
+}
+
+const postAnnouncement = ({ credentials, content, startsAt, endsAt, allDay }) => {
   return promisedRequest({
     url: PLEROMA_POST_ANNOUNCEMENT_URL,
     credentials,
     method: 'POST',
-    payload
+    payload: announcementToPayload({ content, startsAt, endsAt, allDay })
+  })
+}
+
+const editAnnouncement = ({ id, credentials, content, startsAt, endsAt, allDay }) => {
+  return promisedRequest({
+    url: PLEROMA_EDIT_ANNOUNCEMENT_URL(id),
+    credentials,
+    method: 'PATCH',
+    payload: announcementToPayload({ content, startsAt, endsAt, allDay })
   })
 }
 
@@ -1743,6 +1757,7 @@ const apiService = {
   fetchAnnouncements,
   dismissAnnouncement,
   postAnnouncement,
+  editAnnouncement,
   deleteAnnouncement,
   adminFetchAnnouncements
 }
-- 
cgit v1.2.3-70-g09d2


From 458ce5414363f37df94ebbc672dad784ad8fa1ee Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 17 Mar 2022 17:32:16 -0400
Subject: Mark inactive and active announcements for admin

---
 src/components/announcement/announcement.js  |  3 +++
 src/components/announcement/announcement.vue |  2 ++
 src/modules/announcements.js                 | 33 +++++++++++++++++++++++-----
 3 files changed, 33 insertions(+), 5 deletions(-)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 253e2bb2..893a07e0 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -46,6 +46,9 @@ const Announcement = {
       }
 
       return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+    },
+    inactive () {
+      return this.announcement.inactive
     }
   },
   methods: {
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index 1e4edcca..f3b73d89 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -35,6 +35,8 @@
           v-if="currentUser"
           class="btn button-default"
           :class="{ toggled: isRead }"
+          :disabled="inactive"
+          :title="inactive ? $t('announcements.inactive_message') : ''"
           @click="markAsRead"
         >
           {{ $t('announcements.mark_as_read_action') }}
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
index 1d97c67c..8470fb2a 100644
--- a/src/modules/announcements.js
+++ b/src/modules/announcements.js
@@ -12,11 +12,13 @@ export const mutations = {
     set(state, 'announcements', announcements)
   },
   setAnnouncementRead (state, { id, read }) {
-    if (!state.announcements[id]) {
+    const index = state.announcements.findIndex(a => a.id === id)
+
+    if (index < 0) {
       return
     }
 
-    set(state.announcements[id], 'read', read)
+    set(state.announcements[index], 'read', read)
   },
   setFetchAnnouncementsTimer (state, timer) {
     set(state, 'fetchAnnouncementsTimer', announcements)
@@ -31,9 +33,30 @@ const announcements = {
       const currentUser = store.rootState.users.currentUser
       const isAdmin = currentUser && currentUser.role === 'admin'
 
-      return (isAdmin
-        ? store.rootState.api.backendInteractor.adminFetchAnnouncements()
-        : store.rootState.api.backendInteractor.fetchAnnouncements())
+      const getAnnouncements = async () => {
+        if (!isAdmin) {
+          return store.rootState.api.backendInteractor.fetchAnnouncements()
+        }
+
+        const all = await store.rootState.api.backendInteractor.adminFetchAnnouncements()
+        const visible = await store.rootState.api.backendInteractor.fetchAnnouncements()
+        const visibleObject = visible.reduce((a, c) => {
+          a[c.id] = c
+          return a
+        }, {})
+
+        all.forEach(announcement => {
+          if (!visibleObject[announcement.id]) {
+            announcement.inactive = true
+          } else {
+            announcement.read = visibleObject[announcement.id].read
+          }
+        })
+
+        return all
+      }
+
+      return getAnnouncements()
         .then(announcements => {
           store.commit('setAnnouncements', announcements)
         })
-- 
cgit v1.2.3-70-g09d2


From 5f633bbfa3a79a295955c883a8e734f08def321a Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Sun, 20 Mar 2022 13:49:26 -0400
Subject: Change name for the announcement being edited to editedAnnouncement

---
 src/components/announcement/announcement.js  | 12 ++++++------
 src/components/announcement/announcement.vue |  2 +-
 2 files changed, 7 insertions(+), 7 deletions(-)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 893a07e0..caa0543e 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -9,7 +9,7 @@ const Announcement = {
   data () {
     return {
       editing: false,
-      newAnnouncement: {
+      editedAnnouncement: {
         content: '',
         startsAt: undefined,
         endsAt: undefined,
@@ -65,16 +65,16 @@ const Announcement = {
       return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
     },
     enterEditMode () {
-      this.newAnnouncement.content = this.announcement.pleroma['raw_content']
-      this.newAnnouncement.startsAt = this.announcement['starts_at']
-      this.newAnnouncement.endsAt = this.announcement['ends_at']
-      this.newAnnouncement.allDay = this.announcement['all_day']
+      this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
+      this.editedAnnouncement.startsAt = this.announcement['starts_at']
+      this.editedAnnouncement.endsAt = this.announcement['ends_at']
+      this.editedAnnouncement.allDay = this.announcement['all_day']
       this.editing = true
     },
     submitEdit () {
       this.$store.dispatch('editAnnouncement', {
         id: this.announcement.id,
-        ...this.newAnnouncement
+        ...this.editedAnnouncement
       })
         .then(() => {
           this.editing = false
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index f3b73d89..72d99d85 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -12,7 +12,7 @@
       />
       <announcement-editor
         v-else
-        :announcement="newAnnouncement"
+        :announcement="editedAnnouncement"
       />
     </div>
     <div class="footer">
-- 
cgit v1.2.3-70-g09d2


From 68ef7e4e65ed00b5af85d1b089ca2e629665445a Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Thu, 14 Jul 2022 18:42:28 -0400
Subject: Add published time for announcements

---
 src/components/announcement/announcement.js  | 8 ++++++++
 src/components/announcement/announcement.vue | 3 +++
 2 files changed, 11 insertions(+)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 20f8c62f..993e3655 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -33,6 +33,14 @@ const Announcement = {
     isRead () {
       return this.announcement.read
     },
+    publishedAt () {
+      const time = this.announcement['published_at']
+      if (!time) {
+        return
+      }
+
+      return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+    },
     startsAt () {
       const time = this.announcement['starts_at']
       if (!time) {
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index 72d99d85..823e496c 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -20,6 +20,9 @@
         v-if="!editing"
         class="times"
       >
+        <span v-if="publishedAt">
+          {{ $t('announcements.published_time_display', { time: publishedAt }) }}
+        </span>
         <span v-if="startsAt">
           {{ $t('announcements.start_time_display', { time: startsAt }) }}
         </span>
-- 
cgit v1.2.3-70-g09d2


From 54318d31fe7d9167a4b6e991db4fdaebac24a104 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 15 Jul 2022 19:30:32 -0400
Subject: Fix announcement buttons style on narrow screens

---
 src/components/announcement/announcement.vue | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index 823e496c..2753cf8e 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -124,10 +124,11 @@
   .footer .actions {
     display: flex;
     flex-direction: row;
-    justify-content: space-around;
+    justify-content: space-evenly;
 
     .btn {
-      min-width: 10em;
+      flex: 1;
+      margin: 1em;
     }
   }
 }
-- 
cgit v1.2.3-70-g09d2


From fa69034020078ed0b100bd6b99d79ec07f97c58a Mon Sep 17 00:00:00 2001
From: Tusooa Zhu <tusooa@kazv.moe>
Date: Fri, 15 Jul 2022 19:31:30 -0400
Subject: Prevent announcement buttons from being too wide

---
 src/components/announcement/announcement.vue | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/components/announcement/announcement.vue')

diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index 2753cf8e..5f64232a 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -129,6 +129,7 @@
     .btn {
       flex: 1;
       margin: 1em;
+      max-width: 10em;
     }
   }
 }
-- 
cgit v1.2.3-70-g09d2

