Skip to content

Commit be65389

Browse files
committed
Add ability to archive SMS messages
1 parent c34a731 commit be65389

9 files changed

Lines changed: 203 additions & 16 deletions

File tree

api/pkg/repositories/gorm_message_thread_repository.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,14 @@ func (repository *gormMessageThreadRepository) Index(ctx context.Context, owner
108108
ctx, span := repository.tracer.Start(ctx)
109109
defer span.End()
110110

111-
query := repository.db.Where("owner = ?", owner).Where("is_archived = ?", isArchived)
111+
query := repository.db.Where("owner = ?", owner)
112+
113+
if isArchived {
114+
query.Where("is_archived = ?", isArchived)
115+
} else {
116+
query.Where(repository.db.Where("is_archived = ?", isArchived).Or("is_archived IS NULL"))
117+
}
118+
112119
if len(params.Query) > 0 {
113120
queryPattern := "%" + params.Query + "%"
114121
query.Where(

api/pkg/requests/message_thread_index_request.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import (
1111
// MessageThreadIndex is the payload fetching entities.MessageThread sent between 2 numbers
1212
type MessageThreadIndex struct {
1313
request
14-
Skip string `json:"skip" query:"skip"`
15-
Query string `json:"query" query:"query"`
16-
Limit string `json:"limit" query:"limit"`
17-
Owner string `json:"owner" query:"owner"`
14+
IsArchived string `json:"is_archived" query:"is_archived" example:"false"`
15+
Skip string `json:"skip" query:"skip"`
16+
Query string `json:"query" query:"query"`
17+
Limit string `json:"limit" query:"limit"`
18+
Owner string `json:"owner" query:"owner"`
1819
}
1920

2021
// Sanitize sets defaults to MessageOutstanding
@@ -23,8 +24,12 @@ func (input *MessageThreadIndex) Sanitize() MessageThreadIndex {
2324
input.Limit = "20"
2425
}
2526

26-
input.Query = strings.TrimSpace(input.Query)
27+
if strings.TrimSpace(input.IsArchived) == "" {
28+
input.IsArchived = "false"
29+
}
2730

31+
input.IsArchived = input.sanitizeBool(input.IsArchived)
32+
input.Query = strings.TrimSpace(input.Query)
2833
input.Owner = input.sanitizeAddress(input.Owner)
2934

3035
input.Skip = strings.TrimSpace(input.Skip)
@@ -43,6 +48,7 @@ func (input *MessageThreadIndex) ToGetParams() services.MessageThreadGetParams {
4348
Query: input.Query,
4449
Limit: input.getInt(input.Limit),
4550
},
46-
Owner: input.Owner,
51+
IsArchived: input.getBool(input.IsArchived),
52+
Owner: input.Owner,
4753
}
4854
}

api/pkg/requests/request.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,28 @@ func (input *request) sanitizeAddress(value string) string {
2323
return value
2424
}
2525

26+
// getLimit gets the take as a string
27+
func (input *request) sanitizeBool(value string) string {
28+
value = strings.TrimSpace(value)
29+
if value == "1" || strings.ToLower(value) == "true" {
30+
value = "true"
31+
}
32+
33+
if value == "0" || strings.ToLower(value) == "false" {
34+
value = "false"
35+
}
36+
37+
return value
38+
}
39+
40+
// getLimit gets the take as a string
41+
func (input *request) getBool(value string) bool {
42+
if value == "true" {
43+
return true
44+
}
45+
return false
46+
}
47+
2648
// getLimit gets the take as a string
2749
func (input *request) getInt(value string) int {
2850
val, _ := strconv.Atoi(value)

api/pkg/validators/message_thread_handler_validator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func (validator *MessageThreadHandlerValidator) ValidateMessageThreadIndex(_ con
4444
"numeric",
4545
"min:0",
4646
},
47+
"is_archived": []string{
48+
"required",
49+
"in:true,false",
50+
},
4751
"query": []string{
4852
"max:100",
4953
},

web/components/MessageThread.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
55
color="primary"
66
indeterminate
77
></v-progress-linear>
8+
<div
9+
v-if="!$store.getters.getLoadingThreads && $store.getters.getIsArchived"
10+
class="warning py-1 text-center text-uppercase text-subtitle-1"
11+
>
12+
Archived Messages
13+
</div>
814
<v-sheet
9-
v-if="!$store.getters.getLoadingThreads && threads.length === 0"
15+
v-if="
16+
!$store.getters.getLoadingThreads &&
17+
threads.length === 0 &&
18+
!$store.getters.getIsArchived
19+
"
1020
class="text-center mt-8 mx-3"
1121
:color="$vuetify.breakpoint.mdAndDown ? '#121212' : '#363636'"
1222
>

web/components/MessageThreadHeader.vue

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,33 @@
6666
</v-btn>
6767
</template>
6868
<v-list class="px-2" nav :dense="$vuetify.breakpoint.mdAndDown">
69-
<v-list-item-group>
69+
<v-list-item-group v-model="selectedMenuItem">
70+
<v-list-item @click.prevent="toggleArchive">
71+
<v-list-item-icon class="pl-2">
72+
<v-icon v-if="!$store.getters.getIsArchived" dense
73+
>mdi-package-down</v-icon
74+
>
75+
<v-icon v-if="$store.getters.getIsArchived" dense
76+
>mdi-package-up</v-icon
77+
>
78+
</v-list-item-icon>
79+
<v-list-item-content class="ml-n3">
80+
<v-list-item-title class="pr-16 py-1">
81+
<span
82+
v-if="!$store.getters.getIsArchived"
83+
:class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }"
84+
>
85+
Archived
86+
</span>
87+
<span
88+
v-if="$store.getters.getIsArchived"
89+
:class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }"
90+
>
91+
Unarchived
92+
</span>
93+
</v-list-item-title>
94+
</v-list-item-content>
95+
</v-list-item>
7096
<v-list-item
7197
v-if="$store.getters.getOwner"
7298
:to="{ name: 'messages' }"
@@ -136,6 +162,7 @@ import { Phone } from '~/models/phone'
136162
137163
@Component
138164
export default class MessageThreadHeader extends Vue {
165+
selectedMenuItem = -1
139166
get owners(): Array<SelectItem> {
140167
return this.$store.getters.getPhones.map((phone: Phone): SelectItem => {
141168
return {
@@ -156,9 +183,25 @@ export default class MessageThreadHeader extends Vue {
156183
await this.$store.dispatch('loadThreads')
157184
}
158185
186+
async toggleArchive() {
187+
await this.$store.dispatch('toggleArchive')
188+
189+
setTimeout(() => {
190+
this.selectedMenuItem = -1
191+
}, 1000)
192+
193+
if (this.$route.name !== 'threads') {
194+
await this.$store.dispatch('setThreadId', null)
195+
await this.$router.push({ name: 'threads' })
196+
return
197+
}
198+
await this.$store.dispatch('loadThreads')
199+
}
200+
159201
logout(): void {
160202
this.$fire.auth.signOut().then(() => {
161203
this.$store.dispatch('setAuthUser', null)
204+
this.$store.dispatch('resetState')
162205
this.$store.dispatch('addNotification', {
163206
type: 'info',
164207
message: 'You have successfully logged out',

web/models/message-thread.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface MessageThread {
55
id: string
66
last_message_content: string
77
last_message_id: string
8+
is_archived: boolean
89
order_timestamp: string
910
owner: string
1011
updated_at: string

web/pages/threads/_id.vue

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,53 @@
1111
</span>
1212
</v-toolbar-title>
1313
<v-spacer></v-spacer>
14-
<v-tooltip left>
15-
<template #activator="{ on, attrs }">
16-
<v-btn icon text v-bind="attrs" v-on="on">
14+
<v-menu offset-y>
15+
<template #activator="{ on }">
16+
<v-btn icon text class="mt-2" v-on="on">
1717
<v-icon>mdi-dots-vertical</v-icon>
1818
</v-btn>
1919
</template>
20-
<span>Message Options</span>
21-
</v-tooltip>
20+
<v-list class="px-2" nav :dense="$vuetify.breakpoint.mdAndDown">
21+
<v-list-item-group v-model="selectedMenuItem">
22+
<v-list-item
23+
v-if="
24+
$store.getters.hasThread &&
25+
!$store.getters.getThread.is_archived
26+
"
27+
@click.prevent="archiveThread"
28+
>
29+
<v-list-item-icon class="pl-2">
30+
<v-icon dense>mdi-package-down</v-icon>
31+
</v-list-item-icon>
32+
<v-list-item-content class="ml-n3">
33+
<v-list-item-title class="pr-16 py-1">
34+
<span :class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }">
35+
Archive
36+
</span>
37+
</v-list-item-title>
38+
</v-list-item-content>
39+
</v-list-item>
40+
<v-list-item
41+
v-if="
42+
$store.getters.hasThread &&
43+
$store.getters.getThread.is_archived
44+
"
45+
@click.prevent="unArchiveThread"
46+
>
47+
<v-list-item-icon class="pl-2">
48+
<v-icon dense>mdi-package-up</v-icon>
49+
</v-list-item-icon>
50+
<v-list-item-content class="ml-n3">
51+
<v-list-item-title class="pr-16 py-1">
52+
<span :class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }">
53+
Unarchive
54+
</span>
55+
</v-list-item-title>
56+
</v-list-item-content>
57+
</v-list-item>
58+
</v-list-item-group>
59+
</v-list>
60+
</v-menu>
2261
</v-app-bar>
2362
<v-progress-linear
2463
v-if="$store.getters.getLoadingMessages"
@@ -143,14 +182,16 @@ export default Vue.extend({
143182
middleware: ['auth'],
144183
data() {
145184
const formMessageRules: InputValidationRules = [
146-
(v) => !!v || 'Message is required',
147185
(v) =>
148-
(v && v.length <= 320) || 'Message must be less than 320 characters',
186+
v === '' ||
187+
(v && v.length <= 320) ||
188+
'Message must be less than 320 characters',
149189
]
150190
return {
151191
formMessage: '',
152192
formMessageRules,
153193
submitting: false,
194+
selectedMenuItem: -1,
154195
}
155196
},
156197
@@ -187,6 +228,26 @@ export default Vue.extend({
187228
el.scrollTop = el.scrollHeight + 120
188229
},
189230
231+
archiveThread() {
232+
this.$store.dispatch('updateThread', {
233+
threadId: this.$store.getters.getThread.id,
234+
isArchived: true,
235+
})
236+
setTimeout(() => {
237+
this.selectedMenuItem = -1
238+
}, 1000)
239+
},
240+
241+
unArchiveThread() {
242+
this.$store.dispatch('updateThread', {
243+
threadId: this.$store.getters.getThread.id,
244+
isArchived: false,
245+
})
246+
setTimeout(() => {
247+
this.selectedMenuItem = -1
248+
}, 1000)
249+
},
250+
190251
async sendMessage(event: KeyboardEvent) {
191252
if (event.shiftKey) {
192253
return

web/store/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ export const getters = {
150150
return state.pooling
151151
},
152152

153+
getIsArchived(state: State): boolean {
154+
return state.archivedThreads
155+
},
156+
153157
getNotification(state: State): Notification {
154158
return state.notification
155159
},
@@ -205,6 +209,15 @@ export const mutations = {
205209
state.loadingThreads = true
206210
state.loadingMessages = true
207211
},
212+
213+
setArchivedThreads(state: State, payload: boolean) {
214+
state.archivedThreads = payload
215+
},
216+
217+
resetState(state: State) {
218+
state.archivedThreads = false
219+
state.owner = null
220+
},
208221
}
209222

210223
export type SendMessageRequest = {
@@ -223,11 +236,16 @@ export const actions = {
223236
const response = await axios.get('/v1/message-threads', {
224237
params: {
225238
owner: context.getters.getOwner,
239+
is_archived: context.getters.getIsArchived,
226240
},
227241
})
228242
await context.commit('setThreads', response.data.data)
229243
},
230244

245+
async toggleArchive(context: ActionContext<State, State>) {
246+
await context.commit('setArchivedThreads', !context.getters.getIsArchived)
247+
},
248+
231249
async loadPhones(context: ActionContext<State, State>, force: boolean) {
232250
if (context.getters.getPhones.length > 0 && !force) {
233251
return
@@ -241,6 +259,10 @@ export const actions = {
241259
context.commit('setUser', response.data.data)
242260
},
243261

262+
resetState(context: ActionContext<State, State>) {
263+
context.commit('resetState', false)
264+
},
265+
244266
async getHeartbeat(context: ActionContext<State, State>) {
245267
const response = await axios.get('/v1/heartbeats', {
246268
params: {
@@ -338,4 +360,15 @@ export const actions = {
338360

339361
context.commit('setUser', response.data.data)
340362
},
363+
364+
async updateThread(
365+
context: ActionContext<State, State>,
366+
payload: { threadId: string; isArchived: boolean }
367+
) {
368+
await axios.put(`/v1/message-threads/${payload.threadId}`, {
369+
is_archived: payload.isArchived,
370+
})
371+
await context.commit('setArchivedThreads', payload.isArchived)
372+
await context.dispatch('loadThreads')
373+
},
341374
}

0 commit comments

Comments
 (0)