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

Freegle / iznik-nuxt3 / 70a02a7c-0b5c-46a4-9af8-5fa8328911a5

08 Apr 2026 08:22PM UTC coverage: 45.716% (+1.0%) from 44.766%
70a02a7c-0b5c-46a4-9af8-5fa8328911a5

push

circleci

CircleCI Auto-merge
Auto-merge master to production after successful tests - Original commit: fix: show Account deleted badge in expanded mode header (#226)

4632 of 10326 branches covered (44.86%)

Branch coverage included in aggregate %.

2038 of 4264 relevant lines covered (47.8%)

62.37 hits per line

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

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

199
const route = useRoute()
21✔
200

201
const NewFreegler = defineAsyncComponent(() =>
202
  import('~/components/NewFreegler')
203
)
204

205
const props = defineProps({
206
  id: {
207
    type: Number,
208
    required: true,
209
  },
210
  messageOverride: {
211
    type: Object,
212
    required: false,
213
    default: null,
214
  },
215
})
216

217
const emit = defineEmits(['close', 'sent'])
218

219
const faraway = FAR_AWAY
220

221
const messageStore = useMessageStore()
222
const { me, myid, myGroups } = useMe()
223

224
// Initialize state machine
225
const stateMachine = useReplyStateMachine(props.id)
226

227
// References
228
const form = ref(null)
229
const newUserModal = ref(null)
230
const replyToPostChatButton = ref(null)
231
const breakpoint = ref(null)
232
const emailValidatorRef = ref(null)
233

234
// Fetch the message data
235
await messageStore.fetch(props.id)
21✔
236

237
const message = computed(() => {
21✔
238
  return messageStore?.byId(props.id)
32!
239
})
240

241
const milesaway = computed(() => {
242
  return milesAway(me?.lat, me?.lng, message.value?.lat, message.value?.lng)
32!
243
})
244

245
const fromme = computed(() => {
246
  return message.value?.fromuser === myid.value
39!
247
})
248

249
const alreadyAMember = computed(() => {
250
  let found = false
35✔
251

252
  if (message.value?.groups) {
35!
253
    for (const messageGroup of message.value.groups) {
35✔
254
      Object.keys(myGroups.value).forEach((key) => {
35✔
255
        const group = myGroups.value[key]
23✔
256

257
        if (messageGroup.groupid === group.id) {
23✔
258
          found = true
259
        }
260
      })
261
    }
262
  }
263

264
  return found
35✔
265
})
266

267
const replyToUser = computed(() => {
268
  return message.value?.fromuser
32!
269
})
270

271
// Watch for login state changes to resume authentication flow
272
watch(me, async (newVal, oldVal) => {
21✔
273
  console.log('[MessageReplySection] Login state changed', {
274
    newVal: !!newVal,
275
    oldVal: !!oldVal,
276
    state: stateMachine.state.value,
277
  })
278
  if (
16✔
279
    !oldVal &&
30✔
280
    newVal &&
281
    stateMachine.state.value === ReplyState.AUTHENTICATING
282
  ) {
283
    console.log(
3✔
284
      '[MessageReplySection] User logged in during authentication - resuming'
285
    )
286
    try {
3✔
287
      await stateMachine.onLoginSuccess()
3✔
288
    } catch (e) {
289
      console.error(
×
290
        '[MessageReplySection] onLoginSuccess failed, falling back to COMPOSING:',
291
        e
292
      )
293
    }
294
  }
295
})
296

297
// Watch for chat button ref becoming available
298
watch(replyToPostChatButton, (newVal) => {
299
  if (newVal) {
21✔
300
    console.log('[MessageReplySection] Chat button ref now available')
301
    stateMachine.setRefs({ chatButton: newVal })
302
  }
303
})
304

305
// Watch for form ref
306
watch(form, (newVal) => {
307
  if (newVal) {
21✔
308
    console.log('[MessageReplySection] Form ref now available')
309
    stateMachine.setRefs({ form: newVal })
310
  }
311
})
312

313
// Determine reply source from route for analytics.
314
function getReplySource() {
21✔
315
  const path = route.path
21✔
316
  const query = route.query || {}
21!
317

318
  // Check for email digest/newsletter links.
319
  if (query.src === 'digest' || query.utm_source === 'digest') {
21!
320
    return 'email_digest'
321
  }
322
  if (query.src === 'newsletter' || query.utm_source === 'newsletter') {
21!
323
    return 'email_newsletter'
324
  }
325
  if (query.src || query.utm_source) {
21!
326
    return `email_${query.src || query.utm_source}`
×
327
  }
328

329
  // Determine from route path.
330
  if (path.startsWith('/browse')) {
21!
331
    return 'browse_page'
332
  }
333
  if (path.startsWith('/explore')) {
21✔
334
    return 'explore_page'
335
  }
336
  if (path.match(/^\/message\/\d+/)) {
17!
337
    return 'message_page'
338
  }
339
  if (path.startsWith('/find')) {
×
340
    return 'find_page'
341
  }
342

343
  return 'unknown'
344
}
345

346
// Set refs on mount
347
onMounted(() => {
21✔
348
  console.log('[MessageReplySection] Component mounted, setting refs')
21✔
349
  stateMachine.setRefs({
350
    form: form.value,
351
    chatButton: replyToPostChatButton.value,
352
    emailValidator: emailValidatorRef.value,
353
  })
354

355
  // Set and log the reply source for analytics.
356
  const replySource = getReplySource()
21✔
357
  stateMachine.setReplySource(replySource)
21✔
358
  action('reply_section_viewed', {
359
    message_id: props.id,
360
    reply_source: replySource,
361
    message_type: message.value?.type,
21!
362
    is_logged_in: !!me.value,
363
    route_path: route.path,
364
    route_query: JSON.stringify(route.query || {}),
21!
365
    // Additional diagnostics to verify render completed.
366
    has_message_data: !!message.value,
367
    from_me: fromme.value,
368
    has_form_ref: !!form.value,
369
    has_chat_button_ref: !!replyToPostChatButton.value,
370
    can_send: stateMachine.canSend.value,
371
    state_machine_state: stateMachine.state.value,
372
  })
373
})
374

375
// Watch for state machine completion
376
watch(
377
  () => stateMachine.isComplete.value,
378
  (isComplete) => {
379
    if (isComplete) {
7✔
380
      console.log('[MessageReplySection] Reply flow completed')
381
      sent()
382
    }
383
  }
384
)
385

386
// Watch for welcome modal state to show it
387
watch(
388
  () => stateMachine.showWelcomeModal.value,
389
  async (showModal) => {
2✔
390
    if (showModal) {
2✔
391
      console.log('[MessageReplySection] Showing welcome modal')
392
      await nextTick()
4✔
393
      newUserModal.value?.show()
394
    }
395
  }
396
)
397

398
function validateCollect(value) {
59✔
399
  if (value && value.trim()) {
59✔
400
    return true
401
  }
402
  return 'Please suggest some days and times when you could collect.'
403
}
404

405
function validateReply(value) {
78✔
406
  if (!value?.trim()) {
78!
407
    return 'Please fill out your reply.'
408
  }
409

410
  if (
411
    message.value?.type === 'Offer' &&
412
    value &&
413
    value.length <= 35 &&
414
    value.toLowerCase().includes('still available')
415
  ) {
416
    return (
417
      "You don't need to ask if things are still available. Just write whatever you " +
418
      "would have said next - explain why you'd like it and when you could collect."
419
    )
420
  }
421

422
  return true
423
}
424

425
async function handleSend(callback) {
12✔
426
  console.log('[MessageReplySection] handleSend called')
12✔
427

428
  // Log that the Send button was clicked with state info for debugging.
429
  action('reply_send_clicked', {
430
    message_id: props.id,
431
    has_form_ref: !!form.value,
432
    has_chat_button_ref: !!replyToPostChatButton.value,
433
    has_email_validator_ref: !!emailValidatorRef.value,
434
    is_logged_in: !!me.value,
435
    state_machine_state: stateMachine.state.value,
436
    can_send: stateMachine.canSend.value,
437
    is_processing: stateMachine.isProcessing.value,
438
    has_reply_text: !!stateMachine.replyText.value?.trim(),
24✔
439
    has_collect_text: !!stateMachine.collectText.value?.trim(),
24✔
440
  })
441

442
  // Ensure refs are set before submitting
443
  stateMachine.setRefs({
444
    form: form.value,
445
    chatButton: replyToPostChatButton.value,
446
    emailValidator: emailValidatorRef.value,
447
  })
448
  await stateMachine.submit(callback)
449
}
450

451
function handleNewUserModalOk() {
×
452
  console.log('[MessageReplySection] New user modal closed')
×
453
  stateMachine.closeWelcomeModal()
454
}
455

456
function close() {
×
457
  emit('close')
×
458
}
459

460
function sent() {
7✔
461
  emit('sent')
7✔
462
}
463
</script>
464
<style scoped lang="scss">
465
.grey {
466
  background-color: $color-gray--lighter;
467
}
468

469
:deep(.phone) {
470
  border: 2px solid $color-gray--normal !important;
471
}
472

473
.nobot {
474
  margin-bottom: 0 !important;
475
}
476
</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