• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-nuxt3 / a683037b-dce3-48ca-a4dc-f05c54978ab5

21 Aug 2025 03:42PM UTC coverage: 48.844% (-0.3%) from 49.179%
a683037b-dce3-48ca-a4dc-f05c54978ab5

push

circleci

edwh
Test fix.

2230 of 5686 branches covered (39.22%)

Branch coverage included in aggregate %.

5247 of 9622 relevant lines covered (54.53%)

122.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.95
/components/MessageReplySection.vue
1
<template>
2
  <div>
3
    <div v-if="!fromme" class="grey p-2">
4
      <EmailValidator
5
        v-if="!me"
6
        v-model:email="email"
7
        v-model:valid="emailValid"
8
        size="lg"
9
        label="Your email address:"
10
      />
11
      <NoticeMessage
12
        v-if="milesaway > faraway && message.type === 'Offer'"
13
        variant="danger"
14
        class="mt-2 mb-1"
15
      >
16
        This item is {{ milesaway }} miles away. Before replying, are you sure
17
        you can collect from there?
18
      </NoticeMessage>
19
      <NoticeMessage v-if="me?.deleted" variant="danger" class="mt-2 mb-1">
20
        You can't reply until you've decided whether to restore your account.
21
      </NoticeMessage>
22
      <div v-else>
23
        <VeeForm ref="form">
24
          <b-form-group
25
            class="flex-grow-1"
26
            label="Your reply:"
27
            :label-for="'replytomessage-' + message.id"
28
            :description="
29
              message.type === 'Offer'
30
                ? 'Explain why you\'d like it.  It\'s not always first come first served.  If appropriate, ask if it\'s working. Always be polite.'
31
                : 'Can you help?  If you have what they\'re looking for, let them know.'
32
            "
33
          >
34
            <div class="d-flex flex-wrap">
35
              <div v-if="message.deliverypossible" class="mb-2 mr-2">
36
                <b-badge
37
                  v-b-tooltip="
38
                    'They have said they may be able to deliver.  No guarantees - it needs to be convenient for them - but you can ask.'
39
                  "
40
                  variant="info"
41
                  ><v-icon icon="info-circle" /> Delivery may be
42
                  possible</b-badge
43
                >
44
              </div>
45
              <MessageDeadline :id="id" class="mb-2" />
46
            </div>
47
            <Field
48
              v-if="message.type == 'Offer'"
49
              :id="'replytomessage-' + message.id"
50
              v-model="reply"
51
              name="reply"
52
              :rules="validateReply"
53
              as="textarea"
54
              rows="3"
55
              max-rows="8"
56
              class="border border-success w-100"
57
            />
58
            <Field
59
              v-if="message.type == 'Wanted'"
60
              :id="'replytomessage-' + message.id"
61
              v-model="reply"
62
              name="reply"
63
              :rules="validateReply"
64
              as="textarea"
65
              rows="3"
66
              max-rows="8"
67
              class="flex-grow-1 w-100"
68
            />
69
          </b-form-group>
70
          <ErrorMessage name="reply" class="text-danger font-weight-bold" />
71
          <b-form-group
72
            v-if="message.type == 'Offer'"
73
            class="mt-1"
74
            label="When could you collect?"
75
            :label-for="'replytomessage2-' + message.id"
76
            description="Suggest days and times you could collect if you're chosen.  Your plans might change but this speeds up making arrangements."
77
          >
78
            <Field
79
              :id="'replytomessage2-' + message.id"
80
              v-model="collect"
81
              name="collect"
82
              :rules="validateCollect"
83
              class="border border-success w-100"
84
              as="textarea"
85
              rows="2"
86
              max-rows="2"
87
            />
88
          </b-form-group>
89
          <ErrorMessage name="collect" class="text-danger font-weight-bold" />
90
        </VeeForm>
91
        <p v-if="me && !alreadyAMember" class="text--small text-muted">
92
          You're not yet a member of this community; we'll join you. Change
93
          emails or leave communities from
94
          <em>Settings</em>.
95
        </p>
96
      </div>
97
      <div v-if="!me">
98
        <NewFreegler class="mt-2" />
99
      </div>
100
    </div>
101
    <hr />
102
    <div class="d-flex justify-content-between">
103
      <div class="pr-2 w-50">
104
        <b-button variant="secondary" size="lg" block @click="close">
105
          Close
106
        </b-button>
107
      </div>
108
      <div
109
        v-if="!fromme && !me?.deleted"
110
        class="pl-2 w-50 justify-content-end d-flex"
111
      >
112
        <SpinButton
113
          v-if="!me"
114
          variant="primary"
115
          size="lg"
116
          done-icon=""
117
          icon-name="angle-double-right"
118
          :disabled="disableSend"
119
          iconlast
120
          @handle="registerOrSend"
121
        >
122
          Send <span class="d-none d-md-inline">your</span> reply
123
        </SpinButton>
124
        <SpinButton
125
          v-else
126
          variant="primary"
127
          size="lg"
128
          done-icon=""
129
          icon-name="angle-double-right"
130
          :disabled="disableSend"
131
          iconlast
132
          @handle="sendReply"
133
        >
134
          Send <span class="d-none d-md-inline">your</span> reply
135
        </SpinButton>
136
      </div>
137
    </div>
138
    <b-modal
139
      v-if="showNewUser"
140
      id="newUserModal"
141
      ref="newUserModal"
142
      scrollable
143
      ok-only
144
      ok-title="Close and Continue"
145
      @hide="sendReply(null)"
146
    >
147
      <template #title>
148
        <h2>Welcome to Freegle!</h2>
149
      </template>
150
      <NewUserInfo :password="newUserPassword" />
151
    </b-modal>
152
    <span ref="breakpoint" class="d-inline d-sm-none" />
153
    <div class="d-none">
154
      <ChatButton ref="replyToPostChatButton" :userid="replyToUser" />
155
    </div>
156
  </div>
157
</template>
158
<script setup>
159
import { Form as VeeForm, Field, ErrorMessage } from 'vee-validate'
160
import {
161
  defineAsyncComponent,
162
  ref,
163
  computed,
164
  watch,
165
  nextTick,
166
  getCurrentInstance,
167
} from 'vue'
168
import { storeToRefs } from 'pinia'
169
import { useMessageStore } from '../stores/message'
170
import { useAuthStore } from '../stores/auth'
171
import { useReplyStore } from '../stores/reply'
172
import { milesAway } from '../composables/useDistance'
173
import { useMe } from '~/composables/useMe'
174
import { useReplyToPost } from '~/composables/useReplyToPost'
175
import EmailValidator from '~/components/EmailValidator'
176
import NewUserInfo from '~/components/NewUserInfo'
177
import ChatButton from '~/components/ChatButton'
178
import SpinButton from '~/components/SpinButton.vue'
179
import NoticeMessage from '~/components/NoticeMessage'
180
import MessageDeadline from '~/components/MessageDeadline'
181
import { FAR_AWAY } from '~/constants'
182

183
const NewFreegler = defineAsyncComponent(() =>
21✔
184
  import('~/components/NewFreegler')
185
)
186

187
const props = defineProps({
1✔
188
  id: {
189
    type: Number,
190
    required: true,
191
  },
192
  messageOverride: {
193
    type: Object,
194
    required: false,
195
    default: null,
196
  },
197
})
198

199
const emit = defineEmits(['close', 'sent'])
1✔
200

201
const faraway = FAR_AWAY
1✔
202

203
const messageStore = useMessageStore()
1✔
204
const authStore = useAuthStore()
1✔
205
const replyStore = useReplyStore()
1✔
206
const { me, myid, myGroups, fetchMe } = useMe()
1✔
207
const { loggedInEver, forceLogin } = storeToRefs(authStore)
1✔
208
const instance = getCurrentInstance()
1✔
209

210
// References
211
const email = ref(null)
1✔
212
const emailValid = ref(false)
1✔
213
const form = ref(null)
1✔
214
const newUserModal = ref(null)
1✔
215
const replyToPostChatButton = ref(null)
1✔
216
const breakpoint = ref(null)
1✔
217

218
// Data
219
const reply = ref(null)
1✔
220
const collect = ref(null)
1✔
221
const replying = ref(false)
1✔
222
const showNewUser = ref(false)
1✔
223
const newUserPassword = ref(null)
1✔
224
const pendingReply = ref(false)
1✔
225

226
// Fetch the message data
227
await messageStore.fetch(props.id)
1✔
228

1✔
229
// Use the replyToPost composable
230
const message = computed(() => {
231
  return messageStore?.byId(props.id)
4!
232
})
233

234
const milesaway = computed(() => {
1✔
235
  return milesAway(me?.lat, me?.lng, message.value?.lat, message.value?.lng)
4!
236
})
237

238
const disableSend = computed(() => {
1✔
239
  return replying.value
2✔
240
})
241

242
const fromme = computed(() => {
1✔
243
  return message.value?.fromuser === myid.value
5!
244
})
245

246
const alreadyAMember = computed(() => {
1✔
247
  let found = false
6✔
248

249
  if (message.value?.groups) {
6!
250
    for (const messageGroup of message.value.groups) {
5✔
251
      Object.keys(myGroups.value).forEach((key) => {
5✔
252
        const group = myGroups.value[key]
3✔
253

254
        if (messageGroup.groupid === group.id) {
3!
255
          found = true
3✔
256
        }
257
      })
258
    }
259
  }
260

261
  return found
6✔
262
})
263

264
const replyToUser = computed(() => {
1✔
265
  return message.value?.fromuser
4!
266
})
267

268
// Watch for changes in login state
269
watch(me, (newVal, oldVal) => {
1✔
270
  console.log('Login change', newVal, oldVal)
4✔
271
  if (!oldVal && newVal && reply.value) {
4✔
272
    // We have now logged in - resume our send.
273
    console.log('Resume send')
1✔
274
    sendReply()
1✔
275
  }
276
})
277

1✔
278
// Watch for chat button ref to become available when we have a pending reply
279
watch(replyToPostChatButton, async (newVal) => {
280
  if (newVal && pendingReply.value) {
1!
281
    console.log('Chat button ref now available, executing pending reply')
×
282
    pendingReply.value = false
×
283

284
    const { replyToPost: composableReplyToPost } = useReplyToPost()
×
285
    const replySent = await composableReplyToPost(newVal)
×
286
    if (replySent) {
×
287
      sent()
×
288
    }
289
  }
290
})
291

292
function validateCollect(value) {
8✔
293
  if (value && value.trim()) {
8✔
294
    return true
7✔
295
  }
296
  return 'Please suggest some days and times when you could collect.'
1✔
297
}
298

299
function validateReply(value) {
8✔
300
  if (!value?.trim()) {
8✔
301
    return 'Please fill out your reply.'
1✔
302
  }
303

304
  if (
8!
305
    message.value?.type === 'Offer' &&
306
    value &&
307
    value.length <= 35 &&
308
    value.toLowerCase().includes('still available')
309
  ) {
310
    return (
×
311
      "You don't need to ask if things are still available. Just write whatever you " +
312
      "would have said next - explain why you'd like it and when you could collect."
313
    )
314
  }
315

316
  return true
7✔
317
}
318

319
async function registerOrSend(callback) {
1✔
320
  if (!me && !emailValid.value) {
1!
321
    email.value?.focus()
×
322
  }
323

324
  // We've got a reply and an email address. Maybe the email address is a registered user, maybe it's new. If
325
  // it's a registered user then we want to force them to log in.
326
  //
327
  // We attempt to register the user. If the user already exists, then we'll be told about that as an error.
328
  console.log('Register or send', email.value)
1✔
329
  const validate = await form.value.validate()
1✔
330

331
  if (validate.valid) {
1!
332
    try {
1✔
333
      const ret = await instance.proxy.$api.user.add(email.value, false)
1✔
334

335
      console.log('Returned', ret)
1✔
336
      if (ret.ret === 0 && ret.password) {
1!
337
        // We registered a new user and logged in.
338
        loggedInEver.value = true
1✔
339

340
        await fetchMe(true)
1✔
341

342
        // Show the new user modal.
343
        newUserPassword.value = ret.password
1✔
344
        showNewUser.value = true
1✔
345

346
        await nextTick()
1✔
347
        callback()
1✔
348

349
        // Now that we are logged in, we can reply.
350
        newUserModal.value?.show()
1!
351

352
        // Once the modal is closed, we will send the reply.
353
      } else {
354
        // If anything else happens, then we call sendReply which will force us to log in. Then the watch will
355
        // spot that we're logged in and trigger the send, so we don't need to do that here.
356
        console.log('Failed to register - force login', ret)
×
357
        callback()
×
358
        forceLogin.value = true
×
359
      }
360
    } catch (e) {
361
      // Probably an existing user. Force ourselves to log in as above.
362
      console.log('Register exception, force login', e.message)
×
363
      callback()
×
364
      forceLogin.value = true
×
365
    }
366
  } else {
367
    callback()
×
368
  }
369
}
370

371
async function sendReply(callback) {
3✔
372
  console.log('sendReply', reply.value)
3✔
373
  const validate = await form.value.validate()
3✔
374
  let called = false
3✔
375

376
  if (validate.valid) {
3!
377
    if (reply.value) {
3!
378
      // Save the reply
379
      replyStore.replyMsgId = props.id
3✔
380
      replyStore.replyMessage = reply.value
3✔
381

382
      if (collect.value) {
3!
383
        replyStore.replyMessage +=
3✔
384
          '\r\n\r\nPossible collection times: ' + collect.value
385
      }
386

387
      replyStore.replyingAt = Date.now()
3✔
388
      console.log(
3✔
389
        'State',
390
        replyStore.replyMsgId,
391
        replyStore.replyMessage,
392
        replyStore.replyingAt
393
      )
394

395
      if (myid.value) {
3!
396
        // We have several things to do:
397
        // - join a group if need be (doesn't matter which)
398
        // - post our reply
399
        // - show/go to the open the popup chat so they see what happened
400
        replying.value = true
3✔
401
        let found = false
3✔
402
        let tojoin = null
3✔
403

404
        // We shouldn't need to fetch, but we've seen a Sentry issue where the message groups are not valid.
405
        const msg = await messageStore.fetch(props.id, true)
3✔
406

407
        if (msg?.groups) {
3!
408
          for (const messageGroup of msg.groups) {
3✔
409
            tojoin = messageGroup.groupid
3✔
410
            Object.keys(myGroups.value).forEach((key) => {
3✔
411
              const group = myGroups.value[key]
×
412

413
              if (messageGroup.groupid === group.id) {
×
414
                found = true
×
415
              }
416
            })
417
          }
418

419
          if (!found) {
3!
420
            // Not currently a member.
421
            await authStore.joinGroup(myid.value, tojoin, false)
3✔
422
          }
423

424
          // Now we can send the reply via chat.
425
          await nextTick()
3✔
426
          if (callback) {
3!
427
            callback()
×
428
            called = true
×
429
          }
430

431
          // Check if chat button ref is available, if not set pending flag for watch to handle
432
          if (replyToPostChatButton.value) {
3!
433
            const { replyToPost: composableReplyToPost } = useReplyToPost()
3✔
434
            const replySent = await composableReplyToPost(
3✔
435
              replyToPostChatButton.value
436
            )
437
            if (replySent) {
3!
438
              sent()
3✔
439
            }
440
          } else {
441
            console.log(
×
442
              'Chat button ref not available yet, setting pending flag'
443
            )
444
            pendingReply.value = true
×
445
          }
446
        }
447
      } else {
448
        // We're not logged in yet. We need to force a log in. Once that completes then either the watch in here
449
        // or default.vue will spot we have a reply to send and make it happen.
450
        console.log('Force login')
×
451
        forceLogin.value = true
×
452
      }
453
    }
454
  }
455

456
  if (!called && callback) {
3!
457
    callback()
×
458
  }
459
}
460

461
function close() {
×
462
  emit('close')
×
463
}
464

465
function sent() {
3✔
466
  emit('sent')
3✔
467
}
468
</script>
469
<style scoped lang="scss">
470
.grey {
471
  background-color: $color-gray--lighter;
472
}
473

474
:deep(.phone) {
475
  border: 2px solid $color-gray--normal !important;
476
}
477

478
.nobot {
479
  margin-bottom: 0 !important;
480
}
481
</style>
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc