Skip to content

Commit 9bd5598

Browse files
committed
Add phone number selector
1 parent 53ecaed commit 9bd5598

7 files changed

Lines changed: 234 additions & 41 deletions

File tree

web/components/FirebaseAuth.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default class FirebaseAuth extends Vue {
6565
},
6666
},
6767
// Will use popup for IDP Providers sign-in flow instead of the default, redirect.
68-
signInFlow: this.$vuetify.breakpoint.mdAndDown ? 'redirect' : 'popup',
68+
signInFlow: 'popup',
6969
signInSuccessUrl: window.location.href,
7070
signInOptions: [
7171
// Leave the lines as is for the providers you want to offer your users.

web/components/MessageThread.vue

Lines changed: 68 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,77 @@
11
<template>
2-
<v-list two-line class="px-0 py-0" subheader>
2+
<div>
33
<v-progress-linear
44
v-if="$store.getters.getLoadingThreads"
55
color="primary"
66
indeterminate
77
></v-progress-linear>
8-
<v-list-item-group>
9-
<template v-for="thread in threads">
10-
<v-list-item
11-
:key="thread.id"
12-
:to="'/threads/' + thread.id"
13-
class="py-1"
14-
>
15-
<v-list-item-avatar :color="thread.color">
16-
<v-icon dark> mdi-account </v-icon>
17-
</v-list-item-avatar>
18-
<v-list-item-content>
19-
<v-list-item-title>
20-
{{ thread.contact | phoneNumber }}
21-
</v-list-item-title>
22-
<v-list-item-subtitle>
23-
{{ thread.last_message_content }}
24-
</v-list-item-subtitle>
25-
</v-list-item-content>
26-
<v-list-item-action>
27-
<v-list-item-action-text>
28-
{{ threadDate(thread.order_timestamp) }}
29-
</v-list-item-action-text>
30-
</v-list-item-action>
31-
</v-list-item>
32-
</template>
33-
</v-list-item-group>
34-
</v-list>
8+
<v-sheet
9+
v-if="!$store.getters.getLoadingThreads && threads.length === 0"
10+
class="text-center mt-8 mx-3"
11+
:color="$vuetify.breakpoint.mdAndDown ? '#121212' : '#363636'"
12+
>
13+
<div v-if="$vuetify.breakpoint.mdAndDown">
14+
<v-img
15+
class="mx-auto mb-4"
16+
max-width="80%"
17+
contain
18+
:src="require('assets/img/person-texting.svg')"
19+
></v-img>
20+
<p v-if="$store.getters.getOwner" class="text--secondary">
21+
Start sending messages
22+
</p>
23+
</div>
24+
<v-btn
25+
v-if="$store.getters.getOwner"
26+
class="primary"
27+
:to="{ name: 'messages' }"
28+
>
29+
<v-icon>mdi-plus</v-icon>
30+
New Message
31+
</v-btn>
32+
</v-sheet>
33+
<div v-if="$store.getters.getPhones.length === 0" class="px-4 text-center">
34+
<p>
35+
Install the mobile app on your android phone to start sending messages
36+
</p>
37+
<v-btn class="primary" :href="$store.getters.getAppData.appDownloadUrl">
38+
<v-icon>mdi-download</v-icon>
39+
Install App
40+
</v-btn>
41+
</div>
42+
<v-list two-line class="px-0 py-0" subheader>
43+
<v-list-item-group>
44+
<template v-for="thread in threads">
45+
<v-list-item
46+
:key="thread.id"
47+
:to="'/threads/' + thread.id"
48+
class="py-1"
49+
:class="{
50+
'px-6': $vuetify.breakpoint.mdAndDown,
51+
'px-3': $vuetify.breakpoint.lgAndUp,
52+
}"
53+
>
54+
<v-list-item-avatar :color="thread.color">
55+
<v-icon dark> mdi-account </v-icon>
56+
</v-list-item-avatar>
57+
<v-list-item-content>
58+
<v-list-item-title>
59+
{{ thread.contact | phoneNumber }}
60+
</v-list-item-title>
61+
<v-list-item-subtitle>
62+
{{ thread.last_message_content }}
63+
</v-list-item-subtitle>
64+
</v-list-item-content>
65+
<v-list-item-action>
66+
<v-list-item-action-text>
67+
{{ threadDate(thread.order_timestamp) }}
68+
</v-list-item-action-text>
69+
</v-list-item-action>
70+
</v-list-item>
71+
</template>
72+
</v-list-item-group>
73+
</v-list>
74+
</div>
3575
</template>
3676

3777
<script lang="ts">

web/components/MessageThreadHeader.vue

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
<v-sheet
33
class="pa-4 d-flex"
44
:elevation="$vuetify.breakpoint.lgAndUp ? 0 : 2"
5-
:color="$vuetify.breakpoint.lgAndUp ? 'grey darken-4' : 'primary'"
5+
:color="$vuetify.breakpoint.lgAndUp ? 'grey darken-4' : 'black'"
66
>
7-
<div>
7+
<div :class="{ 'px-2': $vuetify.breakpoint.mdAndDown }">
88
<v-toolbar-title>
9-
<div class="d-flex" style="width: 225px">
9+
<div class="d-flex pt-2" style="width: 225px">
1010
<v-select
1111
outlined
1212
dense
13+
:disabled="owners.length === 0"
14+
placeholder="Phone Numbers"
15+
:class="{ 'mb-n6': !$store.getters.getOwner }"
1316
:items="owners"
1417
:value="$store.getters.getOwner"
1518
@change="onOwnerChanged"
@@ -27,7 +30,7 @@
2730
</div>
2831
</div>
2932
</v-toolbar-title>
30-
<div class="d-flex mt-n4">
33+
<div v-if="$store.getters.getOwner" class="d-flex mt-n4">
3134
<p class="text--secondary mb-n1">
3235
{{ $store.getters.getOwner | phoneCountry }}
3336
</p>
@@ -58,12 +61,28 @@
5861
<v-spacer></v-spacer>
5962
<v-menu offset-y>
6063
<template #activator="{ on }">
61-
<v-btn icon text v-on="on">
64+
<v-btn icon text class="mt-2" v-on="on">
6265
<v-icon>mdi-dots-vertical</v-icon>
6366
</v-btn>
6467
</template>
6568
<v-list class="px-2" nav :dense="$vuetify.breakpoint.mdAndDown">
6669
<v-list-item-group>
70+
<v-list-item
71+
v-if="$store.getters.getOwner"
72+
:to="{ name: 'messages' }"
73+
exact
74+
>
75+
<v-list-item-icon class="pl-2">
76+
<v-icon dense>mdi-plus</v-icon>
77+
</v-list-item-icon>
78+
<v-list-item-content class="ml-n3">
79+
<v-list-item-title class="pr-16 py-1">
80+
<span :class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }">
81+
New Message
82+
</span>
83+
</v-list-item-title>
84+
</v-list-item-content>
85+
</v-list-item>
6786
<v-list-item :to="{ name: 'settings' }" exact>
6887
<v-list-item-icon class="pl-2">
6988
<v-icon dense>mdi-account-cog</v-icon>
@@ -76,6 +95,22 @@
7695
</v-list-item-title>
7796
</v-list-item-content>
7897
</v-list-item>
98+
<v-list-item
99+
v-if="$store.getters.getOwner"
100+
:href="$store.getters.getAppData.appDownloadUrl"
101+
exact
102+
>
103+
<v-list-item-icon class="pl-2">
104+
<v-icon dense>mdi-download</v-icon>
105+
</v-list-item-icon>
106+
<v-list-item-content class="ml-n3">
107+
<v-list-item-title class="pr-16 py-1">
108+
<span :class="{ 'pr-16': $vuetify.breakpoint.mdAndUp }">
109+
Install App
110+
</span>
111+
</v-list-item-title>
112+
</v-list-item-content>
113+
</v-list-item>
79114
<v-list-item @click.prevent="logout">
80115
<v-list-item-icon class="pl-2">
81116
<v-icon dense>mdi-logout</v-icon>
@@ -111,7 +146,6 @@ export default class MessageThreadHeader extends Vue {
111146
}
112147
113148
async onOwnerChanged(owner: string) {
114-
console.log('owner changed')
115149
await this.$store.dispatch('setOwner', owner)
116150
if (this.$route.name !== 'threads') {
117151
await this.$store.dispatch('setThreadId', null)

web/layouts/default.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default class DefaultLayout extends Vue {
2626
poller: number | null = null
2727
2828
get hasDrawer(): boolean {
29-
return !['login', 'index', 'settings'].includes(this.$route.name ?? '')
29+
return ['threads', 'threads-id'].includes(this.$route.name ?? '')
3030
}
3131
3232
mounted() {
@@ -44,14 +44,17 @@ export default class DefaultLayout extends Vue {
4444
await this.$store.dispatch('setPolling', true)
4545
4646
const promises = []
47-
if (this.$store.getters.getUser && this.$store.getters.getOwner) {
47+
if (this.$store.getters.getAuthUser && this.$store.getters.getOwner) {
4848
promises.push(
4949
this.$store.dispatch('loadThreads'),
50-
this.$store.dispatch('getHeartbeat'),
51-
this.$store.dispatch('loadPhones', true)
50+
this.$store.dispatch('getHeartbeat')
5251
)
5352
}
5453
54+
if (this.$store.getters.getAuthUser) {
55+
promises.push(this.$store.dispatch('loadPhones', true))
56+
}
57+
5558
if (this.$store.getters.hasThread && this.$store.getters.getUser) {
5659
promises.push(
5760
this.$store.dispatch(

web/pages/messages/index.vue

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<template>
2+
<v-container fluid class="pa-0" :fill-height="$vuetify.breakpoint.lgAndUp">
3+
<div class="w-full h-full">
4+
<v-app-bar :dense="$vuetify.breakpoint.mdAndDown">
5+
<v-btn icon to="/">
6+
<v-icon>mdi-arrow-left</v-icon>
7+
</v-btn>
8+
<v-toolbar-title>New Message</v-toolbar-title>
9+
</v-app-bar>
10+
<v-container>
11+
<v-row>
12+
<v-col cols="12" md="8" offset-md="2" xl="6" offset-xl="3">
13+
<v-form @submit.prevent="sendMessage">
14+
<v-text-field
15+
v-model="formPhoneNumber"
16+
:disabled="sending"
17+
:error="errors.has('to')"
18+
:error-messages="errors.get('to')"
19+
outlined
20+
placeholder="Recipient phone number e.g +18005550199"
21+
label="Phone Number"
22+
></v-text-field>
23+
<v-textarea
24+
v-model="formContent"
25+
:error="errors.has('content')"
26+
:error-messages="errors.get('content')"
27+
:disabled="sending"
28+
outlined
29+
placeholder="Enter your message here"
30+
label="Content"
31+
></v-textarea>
32+
<v-btn
33+
type="submit"
34+
class="primary"
35+
:disabled="sending"
36+
:block="$vuetify.breakpoint.mdAndDown"
37+
>
38+
<v-icon>mdi-send</v-icon>
39+
Send Message
40+
</v-btn>
41+
</v-form>
42+
</v-col>
43+
</v-row>
44+
</v-container>
45+
</div>
46+
</v-container>
47+
</template>
48+
49+
<script>
50+
import axios from '@/plugins/axios'
51+
52+
export default {
53+
name: 'MessagesIndex',
54+
middleware: ['auth'],
55+
data() {
56+
return {
57+
sending: false,
58+
formPhoneNumber: '',
59+
formContent: '',
60+
errors: new Map(),
61+
}
62+
},
63+
64+
methods: {
65+
sendMessage() {
66+
this.errors = new Map()
67+
this.sending = true
68+
axios
69+
.post('/v1/messages/send', {
70+
to: this.formPhoneNumber,
71+
from: this.$store.getters.getOwner,
72+
content: this.formContent,
73+
})
74+
.then(() => {
75+
this.$store.dispatch('addNotification', {
76+
message: 'Message Sent!',
77+
type: 'success',
78+
})
79+
this.$router.push({ name: 'threads' })
80+
})
81+
.catch((axiosError) => {
82+
const errors = new Map()
83+
const response = axiosError.response
84+
if (response.data.data.content) {
85+
errors.set('content', response.data.data.content)
86+
}
87+
if (response.data.data.to) {
88+
errors.set(
89+
'to',
90+
response.data.data.to.map((x) =>
91+
x.replace('to field', 'phone number field')
92+
)
93+
)
94+
}
95+
if (response.data.data.from) {
96+
this.$store.dispatch('addNotification', {
97+
message: response.data.data.from[0],
98+
type: 'error',
99+
})
100+
}
101+
this.errors = errors
102+
})
103+
.finally(() => {
104+
this.sending = false
105+
})
106+
},
107+
},
108+
}
109+
</script>

web/store/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const state = (): State => ({
6464
export type AppData = {
6565
url: string
6666
name: string
67+
appDownloadUrl: string
6768
documentationUrl: string
6869
githubUrl: string
6970
}
@@ -80,6 +81,7 @@ export const getters = {
8081
}
8182
return {
8283
url,
84+
appDownloadUrl: process.env.APP_DOWNLOAD_URL as string,
8385
documentationUrl: process.env.APP_DOCUMENTATION_URL as string,
8486
githubUrl: process.env.APP_GITHUB_URL as string,
8587
name: process.env.APP_NAME as string,
@@ -211,6 +213,11 @@ export type SendMessageRequest = {
211213

212214
export const actions = {
213215
async loadThreads(context: ActionContext<State, State>) {
216+
if (context.getters.getOwner === null) {
217+
await context.commit('setThreads', [])
218+
return
219+
}
220+
214221
const response = await axios.get('/v1/message-threads', {
215222
params: {
216223
owner: context.getters.getOwner,

web/types.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import firebase from 'firebase/compat'
2-
import Vuetify from 'vuetify/lib'
2+
import { Framework } from 'vuetify'
33

44
interface Firebase {
55
auth: firebase.auth.Auth
@@ -14,7 +14,7 @@ export interface SelectItem {
1414

1515
declare module 'vue/types/vue' {
1616
interface Vue {
17-
$vuetify: typeof Vuetify
17+
$vuetify: Framework
1818
$fire: Firebase
1919
}
2020
}

0 commit comments

Comments
 (0)