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

Freegle / Iznik / 11495

09 May 2026 07:35AM UTC coverage: 69.06% (-3.8%) from 72.847%
11495

Pull #408

circleci

edwh
docs(migration): mark restartproject and repaircafewales as migrated (PR #408)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request #408: feat(batch): migrate check_cgas, visualise, tn_sync + dry-run improvements

9127 of 10554 branches covered (86.48%)

Branch coverage included in aggregate %.

507 of 663 new or added lines in 16 files covered. (76.47%)

11902 existing lines in 138 files now uncovered.

101630 of 149824 relevant lines covered (67.83%)

19.56 hits per line

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

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

199
const route = useRoute()
1✔
200

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

205
const props = defineProps({
1✔
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'])
1✔
218

219
const faraway = FAR_AWAY
1✔
220

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

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

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

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

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

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

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

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

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

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

264
  return found
19✔
265
})
19✔
266

267
const replyToUser = computed(() => {
1✔
268
  return message.value?.fromuser
20✔
269
})
20✔
270

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

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

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

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

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

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

UNCOV
343
  return 'unknown'
×
UNCOV
344
}
×
345

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

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

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

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

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

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

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

UNCOV
422
  return true
×
UNCOV
423
}
×
424

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

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

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

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

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

UNCOV
460
function sent() {
×
UNCOV
461
  emit('sent')
×
UNCOV
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