Merge branch 'develop' into fix/next-message-bubbles

This commit is contained in:
Sivin Varghese
2025-02-07 23:26:24 +05:30
committed by GitHub
24 changed files with 533 additions and 488 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- checkout
- node/install:
node-version: '20.12'
node-version: '23.7'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
+12 -12
View File
@@ -5,30 +5,30 @@
version: '3'
services:
base:
base:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.3.3"
VARIANT: 'ubuntu-22.04'
NODE_VERSION: '23.7.0'
RUBY_VERSION: '3.3.3'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
USER_UID: '1000'
USER_GID: '1000'
image: base:latest
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.3.3"
VARIANT: 'ubuntu-22.04'
NODE_VERSION: '23.7.0'
RUBY_VERSION: '3.3.3'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
USER_UID: '1000'
USER_GID: '1000'
volumes:
- ..:/workspace:cached
+1 -3
View File
@@ -23,12 +23,10 @@ jobs:
bundler-cache: true
- uses: pnpm/action-setup@v4
with:
version: 9.3.0
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 23
cache: 'pnpm'
- name: Install pnpm dependencies
+1 -2
View File
@@ -38,7 +38,6 @@ jobs:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -48,7 +47,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 23
cache: 'pnpm'
- name: Install pnpm dependencies
+3 -7
View File
@@ -19,13 +19,11 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: pnpm/action-setup@v2
with:
version: 9.3.0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 23
cache: 'pnpm'
- name: pnpm
@@ -39,7 +37,7 @@ jobs:
- name: setup env
run: |
cp .env.example .env
- name: Run asset compile
run: bundle exec rake assets:precompile
env:
@@ -47,5 +45,3 @@ jobs:
- name: Size Check
run: pnpm run size
+1 -1
View File
@@ -1 +1 @@
3.1.0
3.2.0
@@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
def index
@conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(last_activity_at: :desc).limit(20)
end
private
@@ -77,7 +77,7 @@ const toggleMessageSignature = () => {
setSignature();
};
// Added this watch to dynamically set signature.
// Added this watch to dynamically set signature on target inbox change.
// Only targetInbox has value and is Advance Editor(used by isEmailOrWebWidgetInbox)
// Set the signature only if the inbox based flag is true
watch(
@@ -86,7 +86,8 @@ watch(
nextTick(() => {
if (newValue && props.isEmailOrWebWidgetInbox) setSignature();
});
}
},
{ immediate: true }
);
const onClickInsertEmoji = emoji => {
@@ -15,18 +15,14 @@ import { useCamelCase } from 'dashboard/composables/useTransformKeys';
* @property {Array} messages - Array of all messages [These are not in camelcase]
*/
const props = defineProps({
readMessages: {
type: Array,
default: () => [],
},
unReadMessages: {
type: Array,
default: () => [],
},
currentUserId: {
type: Number,
required: true,
},
firstUnreadId: {
type: Number,
default: null,
},
isAnEmailChannel: {
type: Boolean,
default: false,
@@ -41,12 +37,8 @@ const props = defineProps({
},
});
const unread = computed(() => {
return useCamelCase(props.unReadMessages, { deep: true });
});
const read = computed(() => {
return useCamelCase(props.readMessages, { deep: true });
const allMessages = computed(() => {
return useCamelCase(props.messages, { deep: true });
});
/**
@@ -123,31 +115,22 @@ const getMessageSpacingClass = (message, messages, index) => {
<template>
<ul class="px-4 bg-n-background">
<slot name="beforeAll" />
<template v-for="(message, index) in read" :key="message.id">
<template v-for="(message, index) in allMessages" :key="message.id">
<slot
v-if="firstUnreadId && message.id === firstUnreadId"
name="unreadBadge"
/>
<Message
v-bind="message"
:is-email-inbox="isAnEmailChannel"
:in-reply-to="getInReplyToMessage(message)"
:group-with-next="shouldGroupWithNext(index, read)"
:group-with-next="shouldGroupWithNext(index, allMessages)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
:class="getMessageSpacingClass(message, read, index)"
data-clarity-mask="True"
/>
</template>
<slot name="beforeUnread" />
<template v-for="(message, index) in unread" :key="message.id">
<Message
v-bind="message"
:in-reply-to="getInReplyToMessage(message)"
:group-with-next="shouldGroupWithNext(index, unread)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
:is-email-inbox="isAnEmailChannel"
:class="getMessageSpacingClass(message, unread, index)"
data-clarity-mask="True"
/>
</template>
<slot name="after" />
</ul>
</template>
@@ -26,7 +26,18 @@ const ccEmail = computed(() => {
});
const senderName = computed(() => {
return sender.value.name ?? '';
const fromEmailAddress = fromEmail.value[0] ?? '';
const senderEmail = sender.value.email ?? '';
if (!fromEmailAddress && !senderEmail) return null;
// if the sender of the conversation and the sender of this particular
// email are the same, only then we return the sender name
if (fromEmailAddress === senderEmail) {
return sender.value.name;
}
return null;
});
const bccEmail = computed(() => {
@@ -59,11 +70,19 @@ const showMeta = computed(() => {
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-11'"
>
<template v-if="showMeta">
<div v-if="fromEmail[0]">
<span :class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'">
{{ senderName }}
</span>
&lt;{{ fromEmail[0] }}&gt;
<div
v-if="fromEmail[0]"
:class="hasError ? 'text-n-ruby-11' : 'text-n-slate-12'"
>
<template v-if="senderName">
<span>
{{ senderName }}
</span>
&lt;{{ fromEmail[0] }}&gt;
</template>
<template v-else>
{{ fromEmail[0] }}
</template>
</div>
<div v-if="toEmail.length">
{{ $t('EMAIL_HEADER.TO') }}: {{ toEmail.join(', ') }}
@@ -44,9 +44,10 @@ onMounted(() => {
});
const formatTime = time => {
if (!time || Number.isNaN(time)) return '00:00';
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
const toggleMute = () => {
@@ -55,7 +56,7 @@ const toggleMute = () => {
};
const onTimeUpdate = () => {
currentTime.value = audioPlayer.value.currentTime;
currentTime.value = audioPlayer.value?.currentTime;
};
const seek = event => {
@@ -243,6 +243,15 @@ export default {
unreadMessageCount() {
return this.currentChat.unread_count || 0;
},
unreadMessageLabel() {
const count =
this.unreadMessageCount > 9 ? '9+' : this.unreadMessageCount;
const label =
this.unreadMessageCount > 1
? 'CONVERSATION.UNREAD_MESSAGES'
: 'CONVERSATION.UNREAD_MESSAGE';
return `${count} ${this.$t(label)}`;
},
isInstagramDM() {
return this.conversationType === 'instagram_direct_message';
},
@@ -492,12 +501,11 @@ export default {
<NextMessageList
v-if="showNextBubbles"
class="conversation-panel"
:read-messages="readMessages"
:un-read-messages="unReadMessages"
:current-user-id="currentUserId"
:first-unread-id="unReadMessages[0]?.id"
:is-an-email-channel="isAnEmailChannel"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:messages="currentChat ? currentChat.messages : []"
:messages="getMessages"
>
<template #beforeAll>
<transition name="slide-up">
@@ -507,15 +515,10 @@ export default {
</li>
</transition>
</template>
<template #beforeUnread>
<template #unreadBadge>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
{{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
{{
unreadMessageCount > 1
? $t('CONVERSATION.UNREAD_MESSAGES')
: $t('CONVERSATION.UNREAD_MESSAGE')
}}
{{ unreadMessageLabel }}
</span>
</li>
</template>
@@ -99,6 +99,9 @@
},
"fallback": {
"CONTENT": "has shared a url"
},
"contact": {
"CONTENT": "Shared contact"
}
},
"CHAT_SORT_BY_FILTER": {
@@ -24,6 +24,7 @@
"READ_MORE": "Read more",
"WROTE": "wrote:",
"FROM": "from",
"EMAIL": "email"
"EMAIL": "email",
"EMAIL_SUBJECT": "subject"
}
}
@@ -35,6 +35,10 @@ const props = defineProps({
type: Number,
default: 0,
},
emailSubject: {
type: String,
default: '',
},
});
const navigateTo = computed(() => {
@@ -49,6 +53,28 @@ const navigateTo = computed(() => {
});
const createdAtTime = dynamicTime(props.createdAt);
const infoItems = computed(() => [
{
label: 'SEARCH.FROM',
value: props.name,
show: !!props.name,
},
{
label: 'SEARCH.EMAIL',
value: props.email,
show: !!props.email,
},
{
label: 'SEARCH.EMAIL_SUBJECT',
value: props.emailSubject,
show: !!props.emailSubject,
},
]);
const visibleInfoItems = computed(() =>
infoItems.value.filter(item => item.show)
);
</script>
<template>
@@ -86,26 +112,18 @@ const createdAtTime = dynamicTime(props.createdAt);
{{ createdAtTime }}
</span>
</div>
<div class="flex gap-2">
<div class="flex flex-wrap gap-x-2 gap-y-1.5">
<h5
v-if="name"
class="m-0 text-sm min-w-0 truncate text-n-slate-12 dark:text-n-slate-12"
>
<span class="text-xs font-norma text-n-slate-11 dark:text-n-slate-11">
{{ $t('SEARCH.FROM') }}:
</span>
{{ name }}
</h5>
<h5
v-if="email"
class="m-0 overflow-hidden text-sm text-n-slate-12 dark:text-n-slate-12 truncate"
v-for="item in visibleInfoItems"
:key="item.label"
class="m-0 text-sm min-w-0 text-n-slate-12 dark:text-n-slate-12 truncate"
>
<span
class="text-xs font-normal text-n-slate-11 dark:text-n-slate-11"
>
{{ $t('SEARCH.EMAIL') }}:
{{ $t(item.label) }}:
</span>
{{ email }}
{{ item.value }}
</h5>
</div>
<slot />
@@ -1,9 +1,10 @@
<script setup>
import { defineProps, computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store.js';
import SearchResultSection from './SearchResultSection.vue';
import SearchResultConversationItem from './SearchResultConversationItem.vue';
defineProps({
const props = defineProps({
conversations: {
type: Array,
default: () => [],
@@ -23,6 +24,13 @@ defineProps({
});
const accountId = useMapGetter('getCurrentAccountId');
const conversationsWithSubject = computed(() => {
return props.conversations.map(conversation => ({
...conversation,
mail_subject: conversation.additional_attributes?.mail_subject || '',
}));
});
</script>
<template>
@@ -34,7 +42,10 @@ const accountId = useMapGetter('getCurrentAccountId');
:is-fetching="isFetching"
>
<ul v-if="conversations.length" class="space-y-1.5 list-none">
<li v-for="conversation in conversations" :key="conversation.id">
<li
v-for="conversation in conversationsWithSubject"
:key="conversation.id"
>
<SearchResultConversationItem
:id="conversation.id"
:name="conversation.contact.name"
@@ -42,6 +53,7 @@ const accountId = useMapGetter('getCurrentAccountId');
:account-id="accountId"
:inbox="conversation.inbox"
:created-at="conversation.created_at"
:email-subject="conversation.mail_subject"
/>
</li>
</ul>
@@ -43,6 +43,7 @@ class Telegram::IncomingMessageService
def process_message_attachments
attach_location
attach_files
attach_contact
end
def update_contact_avatar
@@ -136,6 +137,16 @@ class Telegram::IncomingMessageService
)
end
def attach_contact
return unless contact_card
@message.attachments.new(
account_id: @message.account_id,
file_type: :contact,
fallback_title: contact_card['phone_number'].to_s
)
end
def file
@file ||= visual_media_params || params[:message][:voice].presence || params[:message][:audio].presence || params[:message][:document].presence
end
@@ -154,6 +165,10 @@ class Telegram::IncomingMessageService
@location ||= params.dig(:message, :location).presence
end
def contact_card
@contact_card ||= params.dig(:message, :contact).presence
end
def visual_media_params
params[:message][:photo].presence&.last || params.dig(:message, :sticker, :thumb).presence || params[:message][:video].presence
end
@@ -16,6 +16,8 @@ json.payload do
json.agent do
json.partial! 'agent', formats: [:json], agent: conversation.assignee if conversation.try(:assignee).present?
end
json.additional_attributes conversation.additional_attributes
end
end
end
@@ -70,7 +70,9 @@
Finish Setup
</button>
</div>
<% end %>
<% end %>
</div>
</section>
</main>
</div>
</body>
+7 -7
View File
@@ -2,7 +2,7 @@
# Description: Install and manage a Chatwoot installation.
# OS: Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS
# Script Version: 3.1.0
# Script Version: 3.2.0
# Run this script as root
set -eu -o errexit -o pipefail -o noclobber -o nounset
@@ -19,7 +19,7 @@ fi
# option --output/-o requires 1 argument
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version
OPTIONS=cdhiI:l:rsuwv
CWCTL_VERSION="3.1.0"
CWCTL_VERSION="3.2.0"
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events"
@@ -177,7 +177,7 @@ function install_dependencies() {
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
NODE_MAJOR=23
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg 16" > /etc/apt/sources.list.d/pgdg.list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
@@ -779,15 +779,15 @@ function upgrade_node() {
# Parse major version number
major_version=$(echo "$current_version" | cut -d. -f1)
if [ "$major_version" -ge 20 ]; then
echo "Node.js is already version $current_version (>= 20.x). Skipping Node.js upgrade."
if [ "$major_version" -ge 23 ]; then
echo "Node.js is already version $current_version (>= 23.x). Skipping Node.js upgrade."
return
fi
echo "Upgrading Node.js version to v20.x"
echo "Upgrading Node.js version to v23.x"
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
NODE_MAJOR=23
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
apt-get update
+31 -12
View File
@@ -1,6 +1,12 @@
# pre-build stage
FROM node:23-alpine as node
FROM ruby:3.3.3-alpine3.19 AS pre-builder
ARG NODE_VERSION="23.7.0"
ARG PNPM_VERSION="10.2.0"
ENV NODE_VERSION=${NODE_VERSION}
ENV PNPM_VERSION=${PNPM_VERSION}
# ARG default to production settings
# For development docker-compose file overrides ARGS
ARG BUNDLE_WITHOUT="development:test"
@@ -25,18 +31,24 @@ RUN apk update && apk add --no-cache \
tzdata \
postgresql-dev \
postgresql-client \
nodejs=20.15.1-r0 \
git \
curl \
xz \
&& mkdir -p /var/app \
&& gem install bundler
# Install pnpm and configure environment
RUN wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh - \
&& echo 'export PNPM_HOME="/root/.local/share/pnpm"' >> /root/.shrc \
&& echo 'export PATH="$PNPM_HOME:$PATH"' >> /root/.shrc \
&& export PNPM_HOME="/root/.local/share/pnpm" \
&& export PATH="$PNPM_HOME:$PATH" \
&& pnpm --version
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
RUN npm install -g pnpm@${PNPM_VERSION}
RUN echo 'export PNPM_HOME="/root/.local/share/pnpm"' >> /root/.shrc \
&& echo 'export PATH="$PNPM_HOME:$PATH"' >> /root/.shrc \
&& export PNPM_HOME="/root/.local/share/pnpm" \
&& export PATH="$PNPM_HOME:$PATH" \
&& pnpm --version
# Persist the environment variables in Docker
ENV PNPM_HOME="/root/.local/share/pnpm"
@@ -86,6 +98,10 @@ RUN rm -rf /gems/ruby/3.3.0/cache/*.gem \
# final build stage
FROM ruby:3.3.3-alpine3.19
ARG NODE_VERSION="23.7.0"
ARG PNPM_VERSION="10.2.0"
ENV NODE_VERSION=${NODE_VERSION}
ENV PNPM_VERSION=${PNPM_VERSION}
ARG BUNDLE_WITHOUT="development:test"
ENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT}
@@ -114,11 +130,14 @@ RUN apk update && apk add --no-cache \
vips \
&& gem install bundler
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN if [ "$RAILS_ENV" != "production" ]; then \
apk add --no-cache nodejs=20.15.1-r0; \
# Install pnpm and configure environment
wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh - \
&& source /root/.shrc \
apk add --no-cache curl \
&& ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
&& npm install -g pnpm@${PNPM_VERSION} \
&& pnpm --version; \
fi
+9 -8
View File
@@ -111,7 +111,7 @@
"@iconify-json/ri": "^1.2.3",
"@iconify-json/teenyicons": "^1.2.1",
"@size-limit/file": "^8.2.4",
"@vitest/coverage-v8": "2.0.1",
"@vitest/coverage-v8": "3.0.5",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
@@ -134,13 +134,13 @@
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.13",
"vite": "^5.4.8",
"vite": "^5.4.12",
"vite-plugin-ruby": "^5.0.0",
"vitest": "2.0.1"
"vitest": "3.0.5"
},
"engines": {
"node": "20.x",
"pnpm": "9.x"
"node": "23.x",
"pnpm": "10.x"
},
"husky": {
"hooks": {
@@ -150,8 +150,8 @@
"pnpm": {
"overrides": {
"vite-node": "2.0.1",
"vite": "5.4.8",
"vitest": "2.0.1"
"vite": "5.4.12",
"vitest": "3.0.5"
}
},
"lint-staged": {
@@ -162,5 +162,6 @@
"*.scss": [
"scss-lint"
]
}
},
"packageManager": "pnpm@10.2.0+sha512.0d27364e0139c6aadeed65ada153135e0ca96c8da42123bd50047f961339dc7a758fc2e944b428f52be570d1bd3372455c1c65fa2e7aa0bfbf931190f9552001"
}
+322 -367
View File
File diff suppressed because it is too large Load Diff
@@ -309,5 +309,22 @@ describe Telegram::IncomingMessageService do
expect(telegram_channel.inbox.messages.first.content).to eq('Option 1')
end
end
context 'when valid contact message params' do
it 'creates appropriate conversations, message and contacts' do
params = {
'update_id' => 2_342_342_343_242,
'message' => {
'contact': {
'phone_number': '+918660944581'
}
}.merge(message_params)
}.with_indifferent_access
described_class.new(inbox: telegram_channel.inbox, params: params).perform
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
expect(Contact.all.first.name).to eq('Sojan Jose')
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('contact')
end
end
end
end