From f6d87d0e6ebf959813ab0e77c19916a3dd468af8 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 29 Jan 2025 22:53:29 +0530 Subject: [PATCH 1/3] feat: switch to native arm64 runners for docker ce images (#10789) Emulated docker arm64 builds are failing for community images. This PR - Switches gh action to use native arm64 runners - Merges amd64 and arm64 images to create a multi-arch docker image Fixes https://github.com/chatwoot/chatwoot/issues/10790 --- .github/workflows/publish_foss_docker.yml | 118 ++++++++++++++++++---- 1 file changed, 98 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml index 732b22a28..8149ee290 100644 --- a/.github/workflows/publish_foss_docker.yml +++ b/.github/workflows/publish_foss_docker.yml @@ -5,6 +5,7 @@ # # name: Publish Chatwoot CE docker images + on: push: branches: @@ -12,23 +13,32 @@ on: - master tags: - v* -# pull_request: workflow_dispatch: +env: + DOCKER_REPO: chatwoot/chatwoot + jobs: build: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-22.04-arm + runs-on: ${{ matrix.runner }} env: - GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches + GIT_REF: ${{ github.head_ref || github.ref_name }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Strip enterprise code run: | @@ -39,29 +49,97 @@ jobs: run: | echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile - - name: set docker tag + - name: Set Docker Tags run: | - # Replace forward slashes with hyphens in the ref name SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g') - echo "DOCKER_TAG=chatwoot/chatwoot:$SANITIZED_REF-ce" >> $GITHUB_ENV + if [ "${{ github.ref_name }}" = "master" ]; then + echo "DOCKER_TAG=${DOCKER_REPO}:latest-ce" >> $GITHUB_ENV + else + echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}-ce" >> $GITHUB_ENV + fi - - name: replace docker tag if master - if: github.ref_name == 'master' - run: | - echo "DOCKER_TAG=chatwoot/chatwoot:latest-ce" >> $GITHUB_ENV + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v2 + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile - platforms: linux/amd64, linux/arm64 + platforms: ${{ matrix.platform }} push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - tags: ${{ env.DOCKER_TAG }} + outputs: type=image,name=${{ env.DOCKER_TAG }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + env: + GIT_REF: ${{ github.head_ref || github.ref_name }} + run: | + SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g') + if [ "${{ github.ref_name }}" = "master" ]; then + TAG="${DOCKER_REPO}:latest-ce" + else + TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce" + fi + + docker buildx imagetools create -t $TAG \ + $(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *) + + - name: Inspect image + env: + GIT_REF: ${{ github.head_ref || github.ref_name }} + run: | + SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g') + if [ "${{ github.ref_name }}" = "master" ]; then + TAG="${DOCKER_REPO}:latest-ce" + else + TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce" + fi + + docker buildx imagetools inspect $TAG From 2c1a8e59f54e165a5556a2684a962142837a30e1 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:56:08 +0530 Subject: [PATCH 2/3] chore: Next bubble improvements (#10759) --- .../dashboard/assets/scss/_woot.scss | 20 +++-- .../components-next/avatar/Avatar.vue | 4 +- .../components-next/message/Message.vue | 50 +++++++---- .../components-next/message/MessageList.vue | 19 ++++- .../message/bubbles/Activity.vue | 2 +- .../components-next/message/bubbles/Base.vue | 6 +- .../message/bubbles/BaseAttachment.vue | 4 +- .../message/bubbles/Email/Index.vue | 9 +- .../message/bubbles/Text/Index.vue | 7 +- .../message/chips/AttachmentChips.vue | 32 +++----- .../message/chips/AttachmentGrid.vue | 82 +++++++++++++++++++ .../components-next/message/chips/Audio.vue | 26 +++++- .../components-next/message/chips/File.vue | 17 +++- .../components-next/message/chips/Image.vue | 35 +++++++- .../components-next/message/chips/Video.vue | 34 +++++++- .../components-next/message/provider.js | 2 +- tailwind.config.js | 3 + theme/colors.js | 2 + 18 files changed, 283 insertions(+), 71 deletions(-) create mode 100644 app/javascript/dashboard/components-next/message/chips/AttachmentGrid.vue diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index d736d7b5a..29e3c7c60 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -146,17 +146,19 @@ --solid-2: 255 255 255; --solid-3: 255 255 255; --solid-active: 255 255 255; - --solid-amber: 252 232 193; - --solid-blue: 218 236 255; + --solid-amber: 255 228 181; + --solid-blue: 182 216 255; --solid-iris: 230 231 255; + --solid-purple: 202 202 255; + --solid-red: 255 212 219; --alpha-1: 67, 67, 67, 0.06; - --alpha-2: 201, 202, 207, 0.15; + --alpha-2: 196, 197, 198, 0.22; --alpha-3: 255, 255, 255, 0.96; - --black-alpha-1: 0, 0, 0, 0.12; + --black-alpha-1: 0, 0, 0, 0.08; --black-alpha-2: 0, 0, 0, 0.04; --border-blue: 39, 129, 246, 0.5; - --white-alpha: 255, 255, 255, 0.8; + --white-alpha: 255, 255, 255, 0.84; } .dark { @@ -240,16 +242,18 @@ --gray-12: 238 238 238; --background-color: 18 18 19; + --text-blue: 126 182 255; --border-strong: 52 52 52; --border-weak: 38 38 42; --solid-1: 23 23 26; --solid-2: 29 30 36; --solid-3: 44 45 54; --solid-active: 53 57 66; - --solid-amber: 42 37 30; - --solid-blue: 16 49 91; + --solid-amber: 56 50 41; + --solid-blue: 4 51 101; --solid-iris: 38 42 101; - --text-blue: 126 182 255; + --solid-purple: 51 51 107; + --solid-red: 78 46 55; --alpha-1: 36, 36, 36, 0.8; --alpha-2: 139, 147, 182, 0.15; diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue index 65859b1c7..3cf6d3c88 100644 --- a/app/javascript/dashboard/components-next/avatar/Avatar.vue +++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue @@ -50,16 +50,16 @@ const AVATAR_COLORS = { dark: [ ['#4B143D', '#FF8DCC'], ['#3F220D', '#FFA366'], - ['#2A2A2A', '#ADB1B8'], ['#023B37', '#0BD8B6'], + ['#2A2A2A', '#ADB1B8'], ['#27264D', '#A19EFF'], ['#1D2E62', '#9EB1FF'], ], light: [ ['#FBDCEF', '#C2298A'], ['#FFE0BB', '#99543A'], - ['#E8E8E8', '#60646C'], ['#CCF3EA', '#008573'], + ['#E8E8E8', '#60646C'], ['#EBEBFE', '#4747C2'], ['#E1E9FF', '#3A5BC7'], ], diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue index df0602a49..150c57f85 100644 --- a/app/javascript/dashboard/components-next/message/Message.vue +++ b/app/javascript/dashboard/components-next/message/Message.vue @@ -117,7 +117,7 @@ const props = defineProps({ }, conversationId: { type: Number, required: true }, createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties - currentUserId: { type: Number, required: true }, + currentUserId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties groupWithNext: { type: Boolean, default: false }, inboxId: { type: Number, default: null }, // eslint-disable-line vue/no-unused-properties inboxSupportsReplyTo: { type: Object, default: () => ({}) }, @@ -173,7 +173,11 @@ const variant = computed(() => { return variants[props.messageType] || MESSAGE_VARIANTS.USER; }); -const isMyMessage = computed(() => { +const isBotOrAgentMessage = computed(() => { + if (props.messageType === MESSAGE_TYPES.ACTIVITY) { + return false; + } + // if an outgoing message is still processing, then it's definitely a // message sent by the current user if ( @@ -182,17 +186,15 @@ const isMyMessage = computed(() => { ) { return true; } + const senderId = props.senderId ?? props.sender?.id; const senderType = props.senderType ?? props.sender?.type; if (!senderType || !senderId) { - return false; + return true; } - return ( - senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() && - props.currentUserId === senderId - ); + return senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase(); }); /** @@ -200,7 +202,7 @@ const isMyMessage = computed(() => { * @returns {import('vue').ComputedRef<'left'|'right'|'center'>} The computed orientation */ const orientation = computed(() => { - if (isMyMessage.value) { + if (isBotOrAgentMessage.value) { return ORIENTATION.RIGHT; } @@ -221,8 +223,8 @@ const flexOrientationClass = computed(() => { const gridClass = computed(() => { const map = { - [ORIENTATION.LEFT]: 'grid grid-cols-[24px_1fr]', - [ORIENTATION.RIGHT]: 'grid grid-cols-1fr', + [ORIENTATION.LEFT]: 'grid grid-cols-1fr', + [ORIENTATION.RIGHT]: 'grid grid-cols-[1fr_24px]', }; return map[orientation.value]; @@ -231,13 +233,13 @@ const gridClass = computed(() => { const gridTemplate = computed(() => { const map = { [ORIENTATION.LEFT]: ` - "avatar bubble" - "spacer meta" - `, - [ORIENTATION.RIGHT]: ` "bubble" "meta" `, + [ORIENTATION.RIGHT]: ` + "bubble avatar" + "meta spacer" + `, }; return map[orientation.value]; @@ -251,7 +253,7 @@ const shouldGroupWithNext = computed(() => { const shouldShowAvatar = computed(() => { if (props.messageType === MESSAGE_TYPES.ACTIVITY) return false; - if (orientation.value === ORIENTATION.RIGHT) return false; + if (orientation.value === ORIENTATION.LEFT) return false; return true; }); @@ -414,6 +416,11 @@ const avatarInfo = computed(() => { }; }); +const avatarTooltip = computed(() => { + if (avatarInfo.value.name === '') return ''; + return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`; +}); + const setupHighlightTimer = () => { if (Number(route.query.messageId) !== Number(props.id)) { return; @@ -433,7 +440,7 @@ provideMessageContext({ isPrivate: computed(() => props.private), variant, orientation, - isMyMessage, + isBotOrAgentMessage, shouldGroupWithNext, }); @@ -443,7 +450,7 @@ provideMessageContext({
@@ -479,7 +487,13 @@ provideMessageContext({
{ nextMessageType === MESSAGE_TYPES.TEMPLATE && currentMessageType === MESSAGE_TYPES.TEMPLATE; - if (!hasSameSender || areBothTemplates) return false; + const areBothActivity = + nextMessageType === MESSAGE_TYPES.ACTIVITY && + currentMessageType === MESSAGE_TYPES.ACTIVITY; + + if (!hasSameSender || areBothTemplates || areBothActivity) return false; if (currentMessageType !== nextMessageType) return false; @@ -103,6 +107,17 @@ const getInReplyToMessage = parentMessage => { return replyMessage ? useCamelCase(replyMessage) : null; }; + +const getMessageSpacingClass = (message, messages, index) => { + // For non-activity messages, use groupWithNext logic + if (message.messageType !== MESSAGE_TYPES.ACTIVITY) { + return shouldGroupWithNext(index, messages) ? 'mb-1' : 'mb-6'; + } + + // For activity messages, check next message exists and is also an activity + const nextMessage = messages[index + 1]; + return nextMessage?.messageType === MESSAGE_TYPES.ACTIVITY ? 'mb-2' : 'mb-6'; +}; @@ -128,6 +144,7 @@ const getInReplyToMessage = parentMessage => { :inbox-supports-reply-to="inboxSupportsReplyTo" :current-user-id="currentUserId" :is-email-inbox="isAnEmailChannel" + :class="getMessageSpacingClass(message, unread, index)" data-clarity-mask="True" /> diff --git a/app/javascript/dashboard/components-next/message/bubbles/Activity.vue b/app/javascript/dashboard/components-next/message/bubbles/Activity.vue index d52286543..4e9300711 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/Activity.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/Activity.vue @@ -14,7 +14,7 @@ const readableTime = computed(() => diff --git a/app/javascript/dashboard/components-next/message/bubbles/Text/Index.vue b/app/javascript/dashboard/components-next/message/bubbles/Text/Index.vue index 1a7997f50..439c892dc 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/Text/Index.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/Text/Index.vue @@ -20,12 +20,15 @@ const isEmpty = computed(() => {