diff --git a/.github/workflows/deploy_check.yml b/.github/workflows/deploy_check.yml index 9f295a6c8..9f2ae42d8 100644 --- a/.github/workflows/deploy_check.yml +++ b/.github/workflows/deploy_check.yml @@ -11,6 +11,9 @@ concurrency: group: pr-${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true +permissions: + contents: read + jobs: deployment_check: name: Check Deployment diff --git a/.github/workflows/frontend-fe.yml b/.github/workflows/frontend-fe.yml index 1d1116d0c..3d992662a 100644 --- a/.github/workflows/frontend-fe.yml +++ b/.github/workflows/frontend-fe.yml @@ -8,6 +8,9 @@ on: branches: - develop +permissions: + contents: read + jobs: test: runs-on: ubuntu-22.04 diff --git a/.github/workflows/logging_percentage_check.yml b/.github/workflows/logging_percentage_check.yml index 5c45ba635..cef07cc2f 100644 --- a/.github/workflows/logging_percentage_check.yml +++ b/.github/workflows/logging_percentage_check.yml @@ -10,6 +10,9 @@ concurrency: group: pr-${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true +permissions: + contents: read + jobs: log_lines_check: runs-on: ubuntu-latest diff --git a/.github/workflows/nightly_installer.yml b/.github/workflows/nightly_installer.yml index beef5727c..e0c5ed88e 100644 --- a/.github/workflows/nightly_installer.yml +++ b/.github/workflows/nightly_installer.yml @@ -14,6 +14,9 @@ on: - cron: "0 0 * * *" workflow_dispatch: +permissions: + contents: read + jobs: nightly: runs-on: ubuntu-24.04 diff --git a/.github/workflows/publish_codespace_image.yml b/.github/workflows/publish_codespace_image.yml index 5da4fda05..c1b0e4e28 100644 --- a/.github/workflows/publish_codespace_image.yml +++ b/.github/workflows/publish_codespace_image.yml @@ -3,6 +3,10 @@ name: Publish Codespace Base Image on: workflow_dispatch: +permissions: + contents: read + packages: write + jobs: publish-code-space-image: runs-on: ubuntu-latest diff --git a/.github/workflows/publish_ee_docker.yml b/.github/workflows/publish_ee_docker.yml index 8e2c22481..982054a18 100644 --- a/.github/workflows/publish_ee_docker.yml +++ b/.github/workflows/publish_ee_docker.yml @@ -18,6 +18,9 @@ on: env: DOCKER_REPO: chatwoot/chatwoot +permissions: + contents: read + jobs: build: strategy: diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index 3075a7f3d..994e5cef8 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -18,6 +18,9 @@ on: env: DOCKER_REPO: chatwoot/chatwoot +permissions: + contents: read + jobs: build: strategy: diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml index 7869bf89c..909636a75 100644 --- a/.github/workflows/size-limit.yml +++ b/.github/workflows/size-limit.yml @@ -10,6 +10,9 @@ concurrency: group: pr-${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true +permissions: + contents: read + jobs: test: runs-on: ubuntu-22.04 diff --git a/.github/workflows/test_docker_build.yml b/.github/workflows/test_docker_build.yml index b27d90408..96a6c69ac 100644 --- a/.github/workflows/test_docker_build.yml +++ b/.github/workflows/test_docker_build.yml @@ -7,6 +7,9 @@ on: - master workflow_dispatch: +permissions: + contents: read + jobs: test-build: strategy: diff --git a/app/drops/user_drop.rb b/app/drops/user_drop.rb index 7cafea1bb..cf6f1b6a1 100644 --- a/app/drops/user_drop.rb +++ b/app/drops/user_drop.rb @@ -7,11 +7,15 @@ class UserDrop < BaseDrop @obj.try(:available_name) end + def email + @obj.try(:email) + end + def first_name - @obj.try(:name).try(:split).try(:first).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1 + @obj.try(:name).try(:split).try(:first).try(:capitalize) if @obj.try(:name).try(:split).try(:size).to_i > 1 end def last_name - @obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1 + @obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size).to_i > 1 end end diff --git a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue index f2383551c..229fbec03 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useStore } from 'vuex'; import { useMessageContext } from '../provider.js'; import { VOICE_CALL_STATUS } from '../constants'; +import { useCallSession } from 'dashboard/composables/useCallSession'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import BaseBubble from 'next/message/bubbles/Base.vue'; @@ -29,14 +30,16 @@ const BG_COLOR_MAP = { const { t } = useI18n(); const store = useStore(); -const { call, conversationId, currentUserId } = useMessageContext(); +const { call, conversationId, currentUserId, inboxId } = useMessageContext(); +const { joinCall, endCall, activeCall, hasActiveCall, isJoining } = + useCallSession(); const status = computed(() => call.value?.status); const isOutbound = computed(() => call.value?.direction === 'outgoing'); const isFailed = computed(() => [VOICE_CALL_STATUS.NO_ANSWER, VOICE_CALL_STATUS.FAILED].includes(status.value) ); -const acceptedByAgentId = computed(() => call.value?.accepted_by_agent_id); +const acceptedByAgentId = computed(() => call.value?.acceptedByAgentId); const didCurrentUserAnswer = computed( () => !!acceptedByAgentId.value && acceptedByAgentId.value === currentUserId.value @@ -52,8 +55,7 @@ const conversationAssignee = computed(() => { return conversation?.meta?.assignee || null; }); const displayAgentName = computed(() => { - if (call.value?.accepted_by_agent_name) - return call.value.accepted_by_agent_name; + if (call.value?.acceptedByAgentName) return call.value.acceptedByAgentName; if (acceptedByAgentId.value) { const agent = store.getters['agents/getAgentById'](acceptedByAgentId.value); if (agent?.available_name) return agent.available_name; @@ -104,6 +106,45 @@ const iconName = computed(() => { }); const bgColor = computed(() => BG_COLOR_MAP[status.value] || 'bg-n-teal-9'); + +const callSid = computed(() => call.value?.providerCallId); + +// Show "Join call" when the call is still ringing, no agent has claimed it, +// and the conversation is unassigned or assigned to the current user. Mirrors +// the eligibility used by FloatingCallWidget so the bubble can act as a +// recovery affordance after a refresh or missed widget. +const canJoinCall = computed(() => { + if (status.value !== VOICE_CALL_STATUS.RINGING) return false; + if (isOutbound.value) return false; + if (acceptedByAgentId.value) return false; + if (!callSid.value || !inboxId.value || !conversationId.value) return false; + // Suppress the button once this call is the local active session — the + // message status webhook may lag behind, so we can't rely on `status` alone + // to hide it after a successful join from this client. + if (hasActiveCall.value && activeCall.value?.callSid === callSid.value) + return false; + const assignee = conversationAssignee.value; + if (assignee?.id && assignee.id !== currentUserId.value) return false; + return true; +}); + +const handleJoinCall = async () => { + if (!canJoinCall.value || isJoining.value) return; + + if (hasActiveCall.value && activeCall.value?.callSid !== callSid.value) { + await endCall({ + conversationId: activeCall.value.conversationId, + inboxId: activeCall.value.inboxId, + callSid: activeCall.value.callSid, + }); + } + + await joinCall({ + conversationId: conversationId.value, + inboxId: inboxId.value, + callSid: callSid.value, + }); +};