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

Freegle / iznik-nuxt3 / 99e3f760-c611-4bfd-88c0-b43b43faaba6

05 Dec 2025 11:02PM UTC coverage: 42.545% (+0.4%) from 42.1%
99e3f760-c611-4bfd-88c0-b43b43faaba6

push

circleci

actions-user
Auto-merge production to app-ci-fd (daily scheduled)

Automated merge from production branch after successful tests.

🤖 Automated by GitHub Actions

1890 of 4798 branches covered (39.39%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

305 existing lines in 20 files now uncovered.

2376 of 5229 relevant lines covered (45.44%)

31.51 hits per line

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

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

UNCOV
184
const NewFreegler = defineAsyncComponent(() =>
×
185
  import('~/components/NewFreegler')
186
)
187

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

UNCOV
200
const emit = defineEmits(['close', 'sent'])
×
201

UNCOV
202
const faraway = FAR_AWAY
×
203

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

211
// References
UNCOV
212
const email = ref(null)
×
UNCOV
213
const emailValid = ref(false)
×
UNCOV
214
const form = ref(null)
×
UNCOV
215
const newUserModal = ref(null)
×
UNCOV
216
const replyToPostChatButton = ref(null)
×
UNCOV
217
const breakpoint = ref(null)
×
218

219
// Data
UNCOV
220
const reply = ref(null)
×
UNCOV
221
const collect = ref(null)
×
UNCOV
222
const replying = ref(false)
×
UNCOV
223
const showNewUser = ref(false)
×
UNCOV
224
const newUserPassword = ref(null)
×
UNCOV
225
const pendingReply = ref(false)
×
226

227
// Fetch the message data
UNCOV
228
await messageStore.fetch(props.id)
×
229

230
// Use the replyToPost composable
UNCOV
231
const message = computed(() => {
×
UNCOV
232
  return messageStore?.byId(props.id)
×
233
})
234

UNCOV
235
const milesaway = computed(() => {
×
UNCOV
236
  return milesAway(me?.lat, me?.lng, message.value?.lat, message.value?.lng)
×
237
})
238

UNCOV
239
const disableSend = computed(() => {
×
UNCOV
240
  return replying.value
×
241
})
242

UNCOV
243
const fromme = computed(() => {
×
UNCOV
244
  return message.value?.fromuser === myid.value
×
245
})
246

UNCOV
247
const alreadyAMember = computed(() => {
×
UNCOV
248
  let found = false
×
249

UNCOV
250
  if (message.value?.groups) {
×
UNCOV
251
    for (const messageGroup of message.value.groups) {
×
UNCOV
252
      Object.keys(myGroups.value).forEach((key) => {
×
UNCOV
253
        const group = myGroups.value[key]
×
254

UNCOV
255
        if (messageGroup.groupid === group.id) {
×
UNCOV
256
          found = true
×
257
        }
258
      })
259
    }
260
  }
261

UNCOV
262
  return found
×
263
})
264

UNCOV
265
const replyToUser = computed(() => {
×
UNCOV
266
  return message.value?.fromuser
×
267
})
268

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

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

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

UNCOV
293
function validateCollect(value) {
×
UNCOV
294
  if (value && value.trim()) {
×
UNCOV
295
    return true
×
296
  }
UNCOV
297
  return 'Please suggest some days and times when you could collect.'
×
298
}
299

UNCOV
300
function validateReply(value) {
×
UNCOV
301
  if (!value?.trim()) {
×
UNCOV
302
    return 'Please fill out your reply.'
×
303
  }
304

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

UNCOV
317
  return true
×
318
}
319

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

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

UNCOV
332
  if (validate.valid) {
×
UNCOV
333
    try {
×
UNCOV
334
      const ret = await instance.proxy.$api.user.add(email.value, false)
×
335

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

UNCOV
341
        await fetchMe(true)
×
342

343
        // Show the new user modal.
UNCOV
344
        newUserPassword.value = ret.password
×
UNCOV
345
        showNewUser.value = true
×
346

UNCOV
347
        await nextTick()
×
UNCOV
348
        callback()
×
349

350
        // Now that we are logged in, we can reply.
UNCOV
351
        newUserModal.value?.show()
×
352

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

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

UNCOV
377
  if (validate.valid) {
×
UNCOV
378
    if (reply.value) {
×
379
      // Save the reply
UNCOV
380
      replyStore.replyMsgId = props.id
×
UNCOV
381
      replyStore.replyMessage = reply.value
×
382

UNCOV
383
      if (collect.value) {
×
UNCOV
384
        replyStore.replyMessage +=
×
385
          '\r\n\r\nPossible collection times: ' + collect.value
386
      }
387

UNCOV
388
      replyStore.replyingAt = Date.now()
×
UNCOV
389
      console.log(
×
390
        'State',
391
        replyStore.replyMsgId,
392
        replyStore.replyMessage,
393
        replyStore.replyingAt
394
      )
395

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

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

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

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

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

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

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

UNCOV
457
  if (!called && callback) {
×
458
    callback()
×
459
  }
460
}
461

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

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

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

479
.nobot {
480
  margin-bottom: 0 !important;
481
}
482
</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