diff --git a/client/api_patches/src/controller/buttonReplyController.ts b/client/api_patches/src/controller/buttonReplyController.ts new file mode 100644 index 000000000..6e4ca6a50 --- /dev/null +++ b/client/api_patches/src/controller/buttonReplyController.ts @@ -0,0 +1,167 @@ +import type { Request, Response } from 'express'; + +// The answer to a button of a business's message, sent the way WhatsApp Web +// sends it when the button is tapped. +// +// Read over CDP on 2026-10-21 (WAWebGetInteractiveActionsFromButtons and +// WAWebSendButtonsMsgReplyChatAction): +// - "dynamic": the reply buttons of a plain message (dynamicReplyButtons). +// WAWebSendButtonsMsgReplyChatAction(message, button, chat) sends a +// "buttons response" carrying the button's id. Checked live: the business +// went on with its flow. +// - "native": a quick reply of an interactive (native flow) message. The web +// client sends a text with the button's label that quotes the message and +// carries the button's index and id (sendTextMsgToChat, selectedIndex and +// selectedId), then marks the button as clicked. +// - "template": the buttons of a template message, answered by index by +// WA-JS itself. +// - "list": a row of a list message. The web client sends a list response +// (type list_response) that carries the row's title, description and id, +// quoting the list. +// Links, copied codes and phone numbers are never sent: the window acts on +// them. +// +// Sent once, never retried: a send that threw may have reached the business. +// +// Runs inside the page: self-contained, no closure over anything here. +export async function replyButtonInPage(request: any): Promise { + const root = globalThis as any; + const load = (name: string) => { + try { + return typeof root.require === 'function' ? root.require(name) : undefined; + } catch (_) { + return undefined; + } + }; + const store = load('WAWebMsgCollection')?.MsgCollection; + const frontend = load('WAWebFrontendMsgGetters'); + if (!store || !frontend) return { error: 'unavailable' }; + + let msg = store.get(request.messageId); + if (!msg && typeof root.WPP?.chat?.getMessageById === 'function') { + try { + await root.WPP.chat.getMessageById(request.messageId); + } catch (_) { + // Not in the chat: answered below. + } + msg = store.get(request.messageId); + } + if (!msg) return { error: 'message_not_found' }; + const raw = typeof msg.unsafe === 'function' ? msg.unsafe() : msg; + const chat = frontend.getChat(raw); + if (!chat) return { error: 'unavailable' }; + + if (request.source === 'dynamic') { + const button = raw.replyButtons?.getModelsArray?.()?.[request.index]; + const send = load('WAWebSendButtonsMsgReplyChatAction'); + if (!button) return { error: 'button_not_found' }; + if (typeof send !== 'function') return { error: 'unavailable' }; + await send(msg, button, chat); + return { sent: true }; + } + + if (request.source === 'native') { + const button = raw.interactivePayload?.buttons?.[request.index]; + let params: any = null; + try { + params = JSON.parse(button?.buttonParamsJson); + } catch (_) { + params = null; + } + if (!button || button.name !== 'quick_reply' || !params || typeof params.display_text !== 'string') { + return { error: 'button_not_found' }; + } + if (params.disabled === true) return { error: 'button_used' }; + const sendText = load('WAWebSendTextMsgChatAction')?.sendTextMsgToChat; + const mark = load('WAWebBizInteractiveMessageQuickReplyAction')?.markInteractiveButtonClicked; + if (typeof sendText !== 'function') return { error: 'unavailable' }; + await sendText(chat, params.display_text, { + quotedMsg: raw, selectedIndex: request.index, selectedId: params.id, + }); + if (typeof mark === 'function') { + try { + await mark(raw, request.index); + } catch (_) { + // Only the button's look; the answer was sent. + } + } + return { sent: true }; + } + + if (request.source === 'list') { + const rows = (raw.list?.sections ?? []).flatMap((section: any) => section?.rows ?? []); + const row = rows[request.index]; + if (!row || typeof row.rowId !== 'string' || typeof row.title !== 'string') return { error: 'button_not_found' }; + const me = load('WAWebUserPrefsMeUser')?.getMePnUserOrThrow_DO_NOT_USE; + const MsgKey = load('WAWebMsgKey'); + const ack = load('WAWebAck')?.ACK; + const time = load('WATimeUtils')?.unixTime; + const sender = load('WAWebSendMsgChatAction')?.addAndSendMsgToChat; + const types = load('WAWebMsgType'); + const listType = load('WAWebProtobufsE2E.pb')?.Message$ListResponseMessage$ListType?.SINGLE_SELECT; + if (typeof me !== 'function' || typeof MsgKey?.newId !== 'function' || !ack || typeof time !== 'function' + || typeof sender !== 'function' || !types?.MSG_TYPE?.LIST_RESPONSE || !types?.MsgKind?.ListResponse + || typeof msg.msgContextInfo !== 'function' || listType === undefined) { + return { error: 'unavailable' }; + } + const mePn = me(); + const description = typeof row.description === 'string' ? row.description : ''; + await sender(chat, { + type: types.MSG_TYPE.LIST_RESPONSE, + kind: types.MsgKind.ListResponse, + ack: ack.CLOCK, + from: mePn, + id: new MsgKey({ from: mePn, to: chat.id, id: await MsgKey.newId(), participant: undefined, selfDir: 'out' }), + local: true, + isNewMsg: true, + t: time(), + to: chat.id, + ...msg.msgContextInfo(chat.id), + listResponse: { title: row.title, description, listType, singleSelectReply: { selectedRowId: row.rowId } }, + body: description ? `${row.title}\n${description}` : row.title, + viewMode: 'VISIBLE', + }); + return { sent: true }; + } + + const buttons = raw.hydratedButtons; + if (!Array.isArray(buttons) || !buttons[request.index]?.quickReplyButton) return { error: 'button_not_found' }; + if (typeof root.WPP?.chat?.replyToButtonMessage !== 'function') return { error: 'unavailable' }; + await root.WPP.chat.replyToButtonMessage(raw.id.remote._serialized, request.messageId, { + buttonIndex: request.index, + }); + return { sent: true }; +} + +// "true__" or "false__", a message of a one-to-one chat. +const validMessage = (value: unknown): value is string => + typeof value === 'string' && /^(true|false)_\d+@(c\.us|lid)_[A-Za-z0-9]{8,64}$/.test(value); + +const SOURCES = ['dynamic', 'native', 'template', 'list']; + +const STATUS_OF: Record = { + message_not_found: 404, button_not_found: 404, button_used: 409, +}; + +export async function replyToButton(req: Request, res: Response) { + const { source, index } = req.body || {}; + if (!validMessage(req.params.messageId) || !SOURCES.includes(source) + || !Number.isInteger(index) || index < 0 || index > 11) { + return res.status(400).json({ status: 'error', code: 'invalid_request' }); + } + try { + const page: any = (req.client as any)?.page; + if (!page || page.isClosed?.()) throw new Error('unavailable'); + const answer = await page.evaluate(replyButtonInPage, { messageId: req.params.messageId, source, index }); + if (!answer || typeof answer !== 'object') throw new Error('unavailable'); + if (typeof answer.error === 'string') { + const status = STATUS_OF[answer.error]; + if (!status) throw new Error('unavailable'); + return res.status(status).json({ status: 'error', code: answer.error }); + } + return res.status(200).json({ status: 'success', response: { sent: answer.sent === true } }); + } catch (_) { + // Never the native error: it can carry a chat id or the message's text. + return res.status(503).json({ status: 'error', code: 'reply_button_unavailable' }); + } +} diff --git a/client/api_patches/src/routes/index.ts b/client/api_patches/src/routes/index.ts index 087886eeb..2c3856217 100644 --- a/client/api_patches/src/routes/index.ts +++ b/client/api_patches/src/routes/index.ts @@ -41,6 +41,7 @@ import * as StatusDeleteController from '../controller/statusDeleteController'; import * as CommunityAdminController from '../controller/communityAdminController'; import * as CommerceController from '../controller/commerceController'; import * as OrderConfirmController from '../controller/orderConfirmController'; +import * as ButtonReplyController from '../controller/buttonReplyController'; import * as GroupMembershipRequestsController from '../controller/groupMembershipRequestsController'; import * as MiscController from '../controller/miscController'; import * as NewsletterController from '../controller/newsletterController'; @@ -986,6 +987,10 @@ routes.get('/api/:session/commerce/order-details/:messageId', verifyToken, statu OrderConfirmController.readBuyerOrder); routes.post('/api/:session/commerce/order-details/:messageId/confirm', verifyToken, statusConnection, OrderConfirmController.confirmBuyerOrder); +// A button of a business's message, answered the way WhatsApp Web does +// (buttonReplyController.ts). +routes.post('/api/:session/reply-button/:messageId', verifyToken, statusConnection, + ButtonReplyController.replyToButton); routes.post( '/api/:session/set-cart-enabled', verifyToken, diff --git a/client/api_patches/src/tests/controller/buttonReplyController.test.ts b/client/api_patches/src/tests/controller/buttonReplyController.test.ts new file mode 100644 index 000000000..b2415ddc4 --- /dev/null +++ b/client/api_patches/src/tests/controller/buttonReplyController.test.ts @@ -0,0 +1,148 @@ +import { replyButtonInPage, replyToButton } from '../../controller/buttonReplyController'; + +const ID = 'false_5511900000001@lid_3EB0AAAAAAAAAAAA'; + +function fakePage({ present = true, extra = {}, withSender = true }: any = {}) { + const chat = { id: 'chat' }; + const sendButtonReply = jest.fn(async () => {}); + const sendText = jest.fn(async () => {}); + const mark = jest.fn(async () => {}); + const sendList = jest.fn(async () => {}); + const replyToButtonMessage = jest.fn(async () => ({})); + const button = { id: '2', displayText: 'Home' }; + const raw: any = { + id: { remote: { _serialized: '5511900000001@lid' } }, + replyButtons: { getModelsArray: () => [{ id: '1' }, button] }, + interactivePayload: { buttons: [ + { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Debt', id: 'd1' }) }, + { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: 'Site', url: 'https://x.com' }) }, + { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Used', id: 'u', disabled: true }) }, + ] }, + list: { sections: [{ rows: [{ title: 'Outage', rowId: 'r1' }] }, { rows: [{ title: 'Bills', description: 'd', rowId: 'r2' }] }] }, + hydratedButtons: [{ quickReplyButton: { id: 'q', displayText: 'Yes' } }, { urlButton: { url: 'https://x.com' } }], + ...extra, + }; + const msg = { unsafe: () => raw, msgContextInfo: (to: any) => ({ quotedStanzaID: 'Q', to }) }; + const modules: Record = { + WAWebMsgCollection: { MsgCollection: { get: (id: string) => (present && id === ID ? msg : undefined) } }, + WAWebFrontendMsgGetters: { getChat: () => chat }, + WAWebSendButtonsMsgReplyChatAction: withSender ? sendButtonReply : undefined, + WAWebSendTextMsgChatAction: { sendTextMsgToChat: sendText }, + WAWebBizInteractiveMessageQuickReplyAction: { markInteractiveButtonClicked: mark }, + WAWebUserPrefsMeUser: { getMePnUserOrThrow_DO_NOT_USE: () => 'me' }, + WAWebMsgKey: class { static newId = async () => 'NEWID'; constructor(public parts: any) {} }, + WAWebAck: { ACK: { CLOCK: 0 } }, + WATimeUtils: { unixTime: () => 1790000000 }, + WAWebSendMsgChatAction: { addAndSendMsgToChat: sendList }, + WAWebMsgType: { MSG_TYPE: { LIST_RESPONSE: 'list_response' }, MsgKind: { ListResponse: 'listResponse' } }, + 'WAWebProtobufsE2E.pb': { Message$ListResponseMessage$ListType: { SINGLE_SELECT: 1 } }, + }; + const require = (name: string) => { + if (!(name in modules)) throw new Error('unknown'); + return modules[name]; + }; + return { require, chat, msg, raw, button, sendButtonReply, sendText, mark, sendList, replyToButtonMessage }; +} + +async function inPage(fake: any, source: string, index: number, messageId = ID) { + const root = globalThis as any; + const previous = { require: root.require, WPP: root.WPP }; + root.require = fake.require; + root.WPP = { chat: { replyToButtonMessage: fake.replyToButtonMessage } }; + try { + return await replyButtonInPage({ messageId, source, index }); + } finally { + root.require = previous.require; + root.WPP = previous.WPP; + } +} + +test('a reply button of a plain message is answered the way WhatsApp Web does', async () => { + const fake = fakePage(); + expect(await inPage(fake, 'dynamic', 1)).toEqual({ sent: true }); + expect(fake.sendButtonReply).toHaveBeenCalledTimes(1); + expect(fake.sendButtonReply).toHaveBeenCalledWith(fake.msg, fake.button, fake.chat); +}); + +test('a quick reply of an interactive message quotes it with the button index and id', async () => { + const fake = fakePage(); + expect(await inPage(fake, 'native', 0)).toEqual({ sent: true }); + expect(fake.sendText).toHaveBeenCalledTimes(1); + expect(fake.sendText).toHaveBeenCalledWith(fake.chat, 'Debt', { quotedMsg: fake.raw, selectedIndex: 0, selectedId: 'd1' }); + expect(fake.mark).toHaveBeenCalledWith(fake.raw, 0); +}); + +test('a quick reply is sent even when marking the button fails', async () => { + const fake = fakePage(); + fake.mark.mockRejectedValue(new Error('x')); + expect(await inPage(fake, 'native', 0)).toEqual({ sent: true }); +}); + +test('a button that is not a quick reply, or was used, is never sent', async () => { + const fake = fakePage(); + expect(await inPage(fake, 'native', 1)).toEqual({ error: 'button_not_found' }); + expect(await inPage(fake, 'native', 2)).toEqual({ error: 'button_used' }); + expect(await inPage(fake, 'native', 9)).toEqual({ error: 'button_not_found' }); + expect(fake.sendText).not.toHaveBeenCalled(); +}); + +test('a list row is answered with a list response that quotes the list', async () => { + const fake = fakePage(); + expect(await inPage(fake, 'list', 1)).toEqual({ sent: true }); + expect(fake.sendList).toHaveBeenCalledTimes(1); + const [chat, sent] = fake.sendList.mock.calls[0] as any[]; + expect(chat).toBe(fake.chat); + expect(sent).toMatchObject({ + type: 'list_response', kind: 'listResponse', from: 'me', local: true, quotedStanzaID: 'Q', + listResponse: { title: 'Bills', description: 'd', listType: 1, singleSelectReply: { selectedRowId: 'r2' } }, + body: 'Bills\nd' }); + expect(await inPage(fake, 'list', 5)).toEqual({ error: 'button_not_found' }); + expect(fake.sendList).toHaveBeenCalledTimes(1); +}); + +test('a template quick reply is answered by index through WA-JS', async () => { + const fake = fakePage(); + expect(await inPage(fake, 'template', 0)).toEqual({ sent: true }); + expect(fake.replyToButtonMessage).toHaveBeenCalledWith('5511900000001@lid', ID, { buttonIndex: 0 }); + expect(await inPage(fake, 'template', 1)).toEqual({ error: 'button_not_found' }); + expect(fake.replyToButtonMessage).toHaveBeenCalledTimes(1); +}); + +test('a missing message, button or WhatsApp Web function sends nothing', async () => { + expect(await inPage(fakePage({ present: false }), 'dynamic', 0)).toEqual({ error: 'message_not_found' }); + const fake = fakePage(); + expect(await inPage(fake, 'dynamic', 5)).toEqual({ error: 'button_not_found' }); + expect(await inPage(fakePage({ withSender: false }), 'dynamic', 0)).toEqual({ error: 'unavailable' }); + expect(fake.sendButtonReply).not.toHaveBeenCalled(); +}); + +function route(body: any, messageId = ID, evaluate: any = jest.fn()) { + const res: any = { status: jest.fn(() => res), json: jest.fn(() => res) }; + const page = { evaluate, isClosed: () => false }; + return { res, page, run: () => replyToButton({ params: { messageId }, body, client: { page } } as any, res) }; +} + +test.each([ + ['an id that is not a message id', { source: 'dynamic', index: 0 }, '../x'], + ['an unknown source', { source: 'other', index: 0 }, ID], + ['an index past the limit', { source: 'dynamic', index: 12 }, ID], + ['an index that is not a number', { source: 'dynamic', index: '0' }, ID], +])('the route refuses %s before touching the page', async (_name, body, messageId) => { + const { res, page, run } = route(body, messageId); + await run(); + expect(res.status).toHaveBeenCalledWith(400); + expect(page.evaluate).not.toHaveBeenCalled(); +}); + +test('the route answers with the server code and never the native error', async () => { + const refused = route({ source: 'dynamic', index: 0 }, ID, jest.fn(async () => ({ error: 'button_used' }))); + await refused.run(); + expect(refused.res.status).toHaveBeenCalledWith(409); + const failed = route({ source: 'dynamic', index: 0 }, ID, jest.fn(async () => { throw new Error('chat 5511900000001'); })); + await failed.run(); + expect(failed.res.status).toHaveBeenCalledWith(503); + expect(JSON.stringify(failed.res.json.mock.calls)).not.toContain('5511900000001'); + const sent = route({ source: 'dynamic', index: 0 }, ID, jest.fn(async () => ({ sent: true }))); + await sent.run(); + expect(sent.res.status).toHaveBeenCalledWith(200); +}); diff --git a/client/changelog_en-US.txt b/client/changelog_en-US.txt index 7ab7c08f7..5952af865 100644 --- a/client/changelog_en-US.txt +++ b/client/changelog_en-US.txt @@ -4,6 +4,8 @@ This release brings communities and a reorganized settings window, on top of eve WHAT'S NEW +- Buttons in business messages can be answered: a reply button sends the answer the way WhatsApp Web does (reply buttons of plain messages, quick replies of interactive messages, template buttons and the rows of lists), a link button opens the link, a copy button copies the code and a call button says the number. The buttons appear under the message list when the message is selected. + - WPPConnect Server 2.10.61 is now offered as an update without waiting for a new WinZapp version. It only updates dependencies, and WinZapp's changes to the server are not affected. - macOS updates explain whether verification failed because of a connection, certificate, server or GitHub request-limit problem, and whether the verification file or the official release tag could not be checked. Temporary connection and server failures get one retry; request limits show a wait time and do not trigger repeated requests during that wait. diff --git a/client/changelog_es-ES.txt b/client/changelog_es-ES.txt index eb9cdb5bb..ac3f6a7d2 100644 --- a/client/changelog_es-ES.txt +++ b/client/changelog_es-ES.txt @@ -4,6 +4,8 @@ Esta versión trae las comunidades y una ventana de configuración reorganizada, NOVEDADES +- Ya se pueden usar los botones de los mensajes de empresas: un botón de respuesta envía la respuesta igual que WhatsApp Web (botones de respuesta de mensajes simples, respuestas rápidas de mensajes interactivos, botones de plantilla y filas de listas), un botón de enlace abre el enlace, un botón de copiar copia el código y un botón de llamada dice el número. Los botones aparecen debajo de la lista de mensajes cuando el mensaje está seleccionado. + - WPPConnect Server 2.10.61 ya se ofrece como actualización sin esperar a una nueva versión de WinZapp. Solo actualiza dependencias y no afecta a los cambios que WinZapp hace en el servidor. - Las actualizaciones de macOS explican si la verificación ha fallado por un problema de conexión, certificado, servidor o límite de solicitudes de GitHub, y si no se ha podido comprobar el archivo de verificación o la etiqueta de la versión oficial. Los fallos temporales de conexión y servidor se reintentan una vez; los límites de solicitudes muestran un tiempo de espera y no generan nuevas solicitudes durante ese período. diff --git a/client/changelog_pl.txt b/client/changelog_pl.txt index 19048de71..4706e0d3e 100644 --- a/client/changelog_pl.txt +++ b/client/changelog_pl.txt @@ -4,6 +4,8 @@ W tej wersji pojawiają się społeczności i zreorganizowane okno ustawień, op NOWOŚCI +- Przyciski w wiadomościach firm można już używać: przycisk odpowiedzi wysyła odpowiedź tak samo jak WhatsApp Web (przyciski odpowiedzi zwykłych wiadomości, szybkie odpowiedzi wiadomości interaktywnych, przyciski szablonów i wiersze list), przycisk linku otwiera link, przycisk kopiowania kopiuje kod, a przycisk połączenia odczytuje numer. Przyciski pojawiają się pod listą wiadomości, gdy wiadomość jest zaznaczona. + - WPPConnect Server 2.10.61 jest teraz oferowany jako aktualizacja bez czekania na nową wersję WinZapp. Aktualizuje tylko zależności i nie wpływa na zmiany, które WinZapp wprowadza w serwerze. - Aktualizacje macOS wyjaśniają, czy weryfikacja nie powiodła się z powodu połączenia, certyfikatu, serwera lub limitu żądań GitHub oraz czy nie można sprawdzić pliku weryfikacji czy oficjalnego tagu wydania. Tymczasowe błędy połączenia i serwera powodują jedną ponowną próbę; limity żądań pokazują czas oczekiwania i nie powodują kolejnych żądań w tym czasie. diff --git a/client/changelog_pt-BR.txt b/client/changelog_pt-BR.txt index dd762ce5e..82b31de3f 100644 --- a/client/changelog_pt-BR.txt +++ b/client/changelog_pt-BR.txt @@ -4,6 +4,8 @@ Esta versão traz as comunidades e uma janela de configurações reorganizada, a NOVIDADES +- Os botões das mensagens de empresas agora podem ser usados: um botão de resposta envia a resposta do mesmo jeito que o WhatsApp Web (botões de resposta de mensagens simples, respostas rápidas de mensagens interativas, botões de modelo e linhas de listas), um botão de link abre o link, um botão de copiar copia o código e um botão de ligar diz o número. Os botões aparecem abaixo da lista de mensagens quando a mensagem está selecionada. + - O WPPConnect Server 2.10.61 agora é oferecido como atualização sem esperar por uma nova versão do WinZapp. Ele só atualiza dependências e não afeta as alterações que o WinZapp faz no servidor. - As atualizações do macOS explicam se a verificação falhou por um problema de conexão, certificado, servidor ou limite de solicitações do GitHub, e se não foi possível verificar o arquivo de verificação ou a tag da versão oficial. Falhas temporárias de conexão e servidor são repetidas uma vez; limites de solicitações mostram um tempo de espera e não geram novas solicitações durante esse período. diff --git a/client/changelog_pt-PT.txt b/client/changelog_pt-PT.txt index 858156b24..7ba6d6a0b 100644 --- a/client/changelog_pt-PT.txt +++ b/client/changelog_pt-PT.txt @@ -4,6 +4,8 @@ Esta versão traz as comunidades e uma janela de definições reorganizada, alé NOVIDADES +- Os botões das mensagens de empresas passam a poder ser usados: um botão de resposta envia a resposta da mesma forma que o WhatsApp Web (botões de resposta de mensagens simples, respostas rápidas de mensagens interativas, botões de modelo e linhas de listas), um botão de ligação abre a ligação, um botão de copiar copia o código e um botão de chamada diz o número. Os botões aparecem por baixo da lista de mensagens quando a mensagem está selecionada. + - O WPPConnect Server 2.10.61 passa a ser oferecido como atualização sem esperar por uma nova versão do WinZapp. Só atualiza dependências e não afeta as alterações que o WinZapp faz no servidor. - As atualizações do macOS explicam se a verificação falhou devido a um problema de ligação, certificado, servidor ou limite de pedidos do GitHub, e se não foi possível verificar o ficheiro de verificação ou a etiqueta da versão oficial. As falhas temporárias de ligação e servidor são repetidas uma vez; os limites de pedidos mostram um tempo de espera e não geram novos pedidos durante esse período. diff --git a/client/changelog_ro.txt b/client/changelog_ro.txt index 8fe2c9b08..095901bfe 100644 --- a/client/changelog_ro.txt +++ b/client/changelog_ro.txt @@ -4,6 +4,8 @@ Această versiune aduce comunitățile și o fereastră de setări reorganizată NOUTĂȚI +- Butoanele din mesajele firmelor pot fi acum folosite: un buton de răspuns trimite răspunsul la fel ca WhatsApp Web (butoanele de răspuns ale mesajelor simple, răspunsurile rapide ale mesajelor interactive, butoanele de șablon și rândurile listelor), un buton de link deschide linkul, un buton de copiere copiază codul, iar un buton de apel spune numărul. Butoanele apar sub lista de mesaje când mesajul este selectat. + - WPPConnect Server 2.10.61 este acum oferit ca actualizare, fără a aștepta o nouă versiune WinZapp. Actualizează doar dependențe și nu afectează modificările pe care WinZapp le face în server. - Actualizările macOS explică dacă verificarea a eșuat din cauza conexiunii, certificatului, serverului sau limitei de solicitări GitHub și dacă nu a putut fi verificat fișierul de verificare sau eticheta versiunii oficiale. Erorile temporare de conexiune și server sunt reîncercate o singură dată; limitele de solicitări afișează un timp de așteptare și nu generează solicitări repetate în acel interval. diff --git a/client/changelog_tr-TR.txt b/client/changelog_tr-TR.txt index fb81ea830..a5ab671ad 100644 --- a/client/changelog_tr-TR.txt +++ b/client/changelog_tr-TR.txt @@ -4,6 +4,8 @@ Bu sürüm, 2.1.0.0 sürümündeki her şeye ek olarak toplulukları ve yeniden YENİLİKLER +- İşletme mesajlarındaki düğmeler artık kullanılabiliyor: bir yanıt düğmesi yanıtı WhatsApp Web ile aynı şekilde gönderir (düz mesajların yanıt düğmeleri, etkileşimli mesajların hızlı yanıtları, şablon düğmeleri ve listelerin satırları), bağlantı düğmesi bağlantıyı açar, kopyalama düğmesi kodu kopyalar, arama düğmesi numarayı söyler. Mesaj seçildiğinde düğmeler mesaj listesinin altında görünür. + - WPPConnect Server 2.10.61 artık yeni bir WinZapp sürümü beklenmeden güncelleme olarak sunuluyor. Yalnızca bağımlılıkları günceller ve WinZapp'ın sunucuda yaptığı değişiklikleri etkilemez. - macOS güncellemelerinde doğrulamanın bağlantı, sertifika, sunucu veya GitHub istek sınırı nedeniyle mi başarısız olduğu ve doğrulama dosyasının mı, resmî sürüm etiketinin mi kontrol edilemediği açıklanır. Geçici bağlantı ve sunucu hatalarında bir kez yeniden denenir; istek sınırında bekleme süresi gösterilir ve bu süre boyunca tekrar istek gönderilmez. diff --git a/client/core/business_buttons.py b/client/core/business_buttons.py new file mode 100644 index 000000000..eb7e07864 --- /dev/null +++ b/client/core/business_buttons.py @@ -0,0 +1,160 @@ +"""The buttons of a business's message, and the one request that answers them. + +A business sends buttons in three shapes, and WhatsApp Web answers each one +its own way (read over CDP on 2026-10-21, WAWebGetInteractiveActionsFromButtons +and WAWebSendButtonsMsgReplyChatAction): + +* ``dynamicReplyButtons``: the reply buttons of a plain message. Tapping one + sends a "buttons response" carrying the button's id. +* ``interactivePayload.buttons``: an interactive ("native flow") message. A + quick reply is sent as a text message that quotes it, with the button's + index and id; a link, a copied code and a phone number are never sent. +* ``hydratedButtons``: a template message, answered by index. +* ``list``: a list message; each row is answered with a list response that + carries the row's id. + +Only the server can send an answer (api_patches buttonReplyController.ts); here +the buttons are read off the message and the request is made. A link, a code +and a number are acted on by the window. + +Pure: no wx, no main window. +""" + +from __future__ import annotations + +import json +import re +from urllib.parse import quote + +from core.api_client import api_post + +MAX_BUTTONS = 12 +LABEL_LIMIT = 80 +SOURCES = ("dynamic", "native", "template", "list") +REPLY, URL, COPY, CALL = "reply", "url", "copy", "call" + +_MESSAGE = re.compile(r"(?:true|false)_\d+@(?:c\.us|lid)_[A-Za-z0-9]{8,64}") + + +class ButtonReplyError(Exception): + """A refusal with the server's own code, or "unavailable".""" + + def __init__(self, code="unavailable"): + super().__init__(code) + self.code = code + + +def _text(value): + return value.strip() if isinstance(value, str) else "" + + +def _button(kind, source, index, label, value=""): + label = _text(label) + if not label: + return None + return {"kind": kind, "source": source, "index": index, + "label": label[:LABEL_LIMIT], "value": _text(value)} + + +def _dicts(value): + return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else [] + + +def _dynamic(wpp_msg): + found = [] + for index, raw in enumerate(_dicts(wpp_msg.get("dynamicReplyButtons"))): + text = raw.get("buttonText") + button = _button(REPLY, "dynamic", index, text.get("displayText") if isinstance(text, dict) else "", + raw.get("buttonId") if isinstance(raw.get("buttonId"), str) else "") + if button and button["value"]: + found.append(button) + return found + + +def _native(wpp_msg): + payload = wpp_msg.get("interactivePayload") + found = [] + for index, raw in enumerate(_dicts(payload.get("buttons") if isinstance(payload, dict) else None)): + try: + params = json.loads(raw.get("buttonParamsJson")) + except (TypeError, ValueError): + continue + if not isinstance(params, dict) or params.get("disabled") is True: + continue + name, label = raw.get("name"), params.get("display_text") + if name == "quick_reply": + button = _button(REPLY, "native", index, label, params.get("id")) + elif name == "cta_url": + url = _text(params.get("url")) + button = _button(URL, "native", index, label, url) if url.lower().startswith(("http://", "https://")) else None + elif name == "cta_copy": + button = _button(COPY, "native", index, label, params.get("copy_code")) + elif name == "cta_call": + button = _button(CALL, "native", index, label, params.get("phone_number")) + else: + button = None + if button and (button["kind"] == URL or button["value"] or button["kind"] == REPLY): + found.append(button) + return found + + +def _list(wpp_msg): + listing = wpp_msg.get("list") + found, index = [], 0 + for section in _dicts(listing.get("sections") if isinstance(listing, dict) else None): + for row in _dicts(section.get("rows")): + button = _button(REPLY, "list", index, row.get("title"), + row.get("rowId") if isinstance(row.get("rowId"), str) else "") + if button and button["value"]: + found.append(button) + index += 1 + return found + + +def _template(wpp_msg): + found = [] + for index, raw in enumerate(_dicts(wpp_msg.get("hydratedButtons"))): + quick, link, call = raw.get("quickReplyButton"), raw.get("urlButton"), raw.get("callButton") + if isinstance(quick, dict): + button = _button(REPLY, "template", index, quick.get("displayText"), quick.get("id")) + elif isinstance(link, dict): + url = _text(link.get("url")) + button = _button(URL, "template", index, link.get("displayText"), url) \ + if url.lower().startswith(("http://", "https://")) else None + elif isinstance(call, dict): + button = _button(CALL, "template", index, call.get("displayText"), call.get("phoneNumber")) + else: + button = None + if button: + found.append(button) + return found + + +def business_buttons(wpp_msg): + """The buttons to offer for a message as the server serialized it, in the + order WhatsApp shows them: [{kind, source, index, label, value}]. `index` + is the button's place in its own list, which is what the server answers + by. Empty for a message without any, or for a button it cannot act on.""" + if not isinstance(wpp_msg, dict): + return [] + return (_dynamic(wpp_msg) + _native(wpp_msg) + _template(wpp_msg) + _list(wpp_msg))[:MAX_BUTTONS] + + +def reply_to_button(connection, message_id, button): + """Answer a reply button, once. Never retried: a lost answer may follow + one that was sent.""" + if not isinstance(message_id, str) or not _MESSAGE.fullmatch(message_id): + raise ButtonReplyError("invalid_request") + if not isinstance(button, dict) or button.get("kind") != REPLY or button.get("source") not in SOURCES: + raise ButtonReplyError("invalid_request") + server, port, token = connection + url = f"{server}:{port}/api/{token}/reply-button/{quote(message_id, safe='')}" + try: + response = api_post(url, json={"source": button["source"], "index": button["index"]}, + headers={"Authorization": f"Bearer {token}"}, timeout=45) + body = response.json() + except Exception: + raise ButtonReplyError() from None + if not isinstance(body, dict) or response.status_code != 200 or body.get("status") != "success": + code = body.get("code") if isinstance(body, dict) else None + raise ButtonReplyError(code if isinstance(code, str) and code != "reply_button_unavailable" else "unavailable") diff --git a/client/core/websocket_client.py b/client/core/websocket_client.py index 023159edd..14e20c7cb 100644 --- a/client/core/websocket_client.py +++ b/client/core/websocket_client.py @@ -9,6 +9,7 @@ from core.api_client import api_get, api_post from core.connection_lifecycle import socket_client_is_current from core.call_log import CALL_LOG_MESSAGE_TYPE, call_log_creator, call_log_payload +from core.business_buttons import business_buttons from core.commerce import ( ORDER_MESSAGE_TYPE, ORDER_UPDATE_MESSAGE_TYPE, PRODUCT_MESSAGE_TYPE, order_payload, order_update_payload, product_payload, @@ -3159,6 +3160,9 @@ def _safe_media_key(val): if not _is_edit_event and server_marks_edited(wpp_msg): normalized["_edited"] = True normalized.update(remote_star_state(wpp_msg)) + buttons = business_buttons(wpp_msg) + if buttons: + normalized["businessButtons"] = buttons replies = comment_count(wpp_msg.get("replyCount")) if replies is not None: normalized["replyCount"] = replies diff --git a/client/data/help_guide/en-US.html b/client/data/help_guide/en-US.html index 6fb494aaf..06a4942fa 100644 --- a/client/data/help_guide/en-US.html +++ b/client/data/help_guide/en-US.html @@ -776,6 +776,9 @@

WhatsApp Business accounts

Orders created by the seller

Enter on an order a seller created for you opens its details, with items and subtotal. When the order is pending and is paid outside the app, "Confirm order info" sends the seller the confirmation, like WhatsApp Web; the dialog says which buyer responses WinZapp does not offer yet.

+

Buttons in business messages

+

Some messages from businesses come with buttons. Select the message and press Tab: the buttons are real buttons under the message list, in the order of Tab. A reply button sends the answer to the business, the way WhatsApp Web does, and WinZapp says whether it was sent. The rows of a list message are buttons too, one for each row. A link button opens the link, a copy button copies the code, and a call button says the number to dial. The buttons are the business's own, so their names change from message to message.

+
@@ -1264,6 +1267,7 @@

📝 20. Guide update log

Every new alpha that brings features is added to the guide. This list shows up to which alpha the guide is complete and what was added in each round.

+ diff --git a/client/data/help_guide/es-ES.html b/client/data/help_guide/es-ES.html index 65a4161ca..766c37ab9 100644 --- a/client/data/help_guide/es-ES.html +++ b/client/data/help_guide/es-ES.html @@ -776,6 +776,9 @@

Cuentas de WhatsApp Business

Pedidos creados por el vendedor

Intro en un pedido que un vendedor creó para ti abre sus datos, con artículos y subtotal. Cuando el pedido está pendiente y se paga fuera de la aplicación, "Confirmar información del pedido" envía la confirmación al vendedor, como en WhatsApp Web; el diálogo dice qué respuestas del comprador WinZapp aún no ofrece.

+

Botones en los mensajes de empresas

+

Algunos mensajes de empresas traen botones. Seleccione el mensaje y pulse Tab: los botones son botones de verdad, debajo de la lista de mensajes, en el orden del Tab. Un botón de respuesta envía la respuesta a la empresa, igual que WhatsApp Web, y WinZapp dice si se envió. Las filas de un mensaje de lista también son botones, uno por cada fila. Un botón de enlace abre el enlace, un botón de copiar copia el código y un botón de llamada dice el número que hay que marcar. Los botones son de la propia empresa, así que sus nombres cambian de un mensaje a otro.

+
@@ -1264,6 +1267,7 @@

📝 20. Registro de la guía

Cada alpha nueva que trae funciones se añade a la guía. Esta lista muestra hasta qué alpha está completa la guía y qué se añadió en cada ronda.

AlphaWhat went into the guide
Not yet releasedButtons in business messages: reply, link, copy code and phone number.
Not yet releasedmacOS update verification: distinct causes and verification stages, one bounded transient retry, and GitHub wait times.
Not yet releasedUpdate package retention, installation ownership, copy results and safe reopening. After an incomplete or unconfirmed copy, WinZapp stays closed and opens recovery instructions in Notepad. Answering, rejecting and hanging up calls from any window (Ctrl+Alt+Shift+Enter and Ctrl+Alt+Shift+Backspace). Advanced category, shortcut letters in Settings, shortcuts in a tree with search, draft per conversation, Try sending again, controls after the list that can leave the Tab order, the queue and windows of local transcription, automatic transcription per conversation, statuses that were not posted and deleting a status, groups of a community, orders created by the seller and status privacy.
Not yet releasedAutomatic adaptation of call audio to microphone and speaker sample rates.
+ diff --git a/client/data/help_guide/pl.html b/client/data/help_guide/pl.html index 735219370..58fae1e20 100644 --- a/client/data/help_guide/pl.html +++ b/client/data/help_guide/pl.html @@ -776,6 +776,9 @@

Konta WhatsApp Business

Zamówienia utworzone przez sprzedawcę

Enter na zamówieniu, które sprzedawca utworzył dla Ciebie, otwiera jego dane z pozycjami i sumą częściową. Gdy zamówienie oczekuje i jest opłacane poza aplikacją, "Potwierdź informacje o zamówieniu" wysyła sprzedawcy potwierdzenie, jak w WhatsApp Web; okno mówi, których odpowiedzi kupującego WinZapp jeszcze nie oferuje.

+

Przyciski w wiadomościach firm

+

Niektóre wiadomości od firm mają przyciski. Zaznacz wiadomość i naciśnij Tab: przyciski są prawdziwymi przyciskami pod listą wiadomości, w kolejności klawisza Tab. Przycisk odpowiedzi wysyła odpowiedź do firmy tak samo jak WhatsApp Web, a WinZapp mówi, czy została wysłana. Wiersze wiadomości z listą też są przyciskami, po jednym na wiersz. Przycisk linku otwiera link, przycisk kopiowania kopiuje kod, a przycisk połączenia odczytuje numer do wybrania. Przyciski pochodzą od firmy, więc ich nazwy zmieniają się z wiadomości na wiadomość.

+
@@ -1264,6 +1267,7 @@

📝 20. Dziennik zmian przewodnika

Każda nowa alpha, która wnosi funkcje, jest dodawana do przewodnika. Ta lista pokazuje, do której alphy przewodnik jest kompletny i co dodano w każdej rundzie.

AlphaQué entró en la guía
Aún no publicadoBotones en los mensajes de empresas: respuesta, enlace, copiar código y número de teléfono.
Aún no publicadoVerificación de actualizaciones de macOS: causas y etapas diferenciadas, un reintento limitado para fallos temporales y tiempos de espera de GitHub.
Aún no publicadoConservación del paquete de actualización, exclusividad de la instalación, resultado de la copia y reapertura segura. Tras una copia incompleta o no confirmada, WinZapp permanece cerrado y abre instrucciones de recuperación en el Bloc de notas. Contestar, rechazar y colgar llamadas desde cualquier ventana (Ctrl+Alt+Shift+Enter y Ctrl+Alt+Shift+Retroceso). Categoría Avanzadas, letras de atajo en la Configuración, atajos en un árbol con búsqueda, borrador por conversación, Intentar enviar de nuevo, controles después de la lista que pueden salir del Tab, cola y ventanas de la transcripción local, transcripción automática por conversación, estados que no se publicaron y eliminar un estado, grupos de una comunidad, pedidos creados por el vendedor y privacidad del estado.
Aún no publicadoAdaptación automática del audio de las llamadas a la frecuencia de muestreo del micrófono y del altavoz.
+ diff --git a/client/data/help_guide/pt-BR.html b/client/data/help_guide/pt-BR.html index 7026dd48d..a81d562fe 100644 --- a/client/data/help_guide/pt-BR.html +++ b/client/data/help_guide/pt-BR.html @@ -776,6 +776,9 @@

Contas WhatsApp Business

Pedidos criados pelo vendedor

Enter num pedido que um vendedor criou para você abre os dados dele, com itens e subtotal. Quando o pedido está pendente e é pago fora do aplicativo, "Confirmar informações do pedido" envia a confirmação ao vendedor, como no WhatsApp Web; o diálogo diz quais respostas do comprador o WinZapp ainda não oferece.

+

Botões nas mensagens de empresas

+

Algumas mensagens de empresas vêm com botões. Selecione a mensagem e aperte Tab: os botões são botões de verdade, abaixo da lista de mensagens, na ordem do Tab. Um botão de resposta envia a resposta à empresa, do mesmo jeito que o WhatsApp Web, e o WinZapp diz se ela foi enviada. As linhas de uma mensagem de lista também são botões, um para cada linha. Um botão de link abre o link, um botão de copiar copia o código e um botão de ligar diz o número para discar. Os botões são da própria empresa, então os nomes mudam de mensagem para mensagem.

+
@@ -1264,6 +1267,7 @@

📝 20. Registro de atualizações do guia

Cada alpha nova que traz funcionalidades é acrescentada ao guia. Esta lista mostra até que alpha o guia está completo e o que foi acrescentado em cada rodada.

AlphaCo trafiło do przewodnika
Jeszcze niewydanePrzyciski w wiadomościach firm: odpowiedź, link, kopiowanie kodu i numer telefonu.
Jeszcze niewydaneWeryfikacja aktualizacji macOS: osobne przyczyny i etapy, jedna ograniczona ponowna próba przy błędach tymczasowych i czas oczekiwania GitHub.
Jeszcze niewydaneZachowywanie pakietu aktualizacji, wyłączność instalacji, wynik kopiowania i bezpieczne ponowne otwarcie. Po niepełnym lub niepotwierdzonym kopiowaniu WinZapp pozostaje zamknięty i otwiera instrukcje naprawy w Notatniku. Odbieranie, odrzucanie i rozłączanie połączeń z dowolnego okna (Ctrl+Alt+Shift+Enter i Ctrl+Alt+Shift+Backspace). Kategoria Zaawansowane, litery skrótów w Ustawieniach, skróty w drzewie z wyszukiwaniem, wersja robocza dla każdego czatu, Spróbuj wysłać ponownie, elementy za listą, które mogą wyjść z kolejności klawisza Tab, kolejka i okna lokalnej transkrypcji, automatyczna transkrypcja dla każdego czatu, statusy, które nie zostały opublikowane, i usuwanie statusu, grupy społeczności, zamówienia utworzone przez sprzedawcę i prywatność statusu.
Jeszcze niewydaneAutomatyczne dostosowanie dźwięku połączeń do częstotliwości próbkowania mikrofonu i głośnika.
+ diff --git a/client/data/help_guide/pt-PT.html b/client/data/help_guide/pt-PT.html index 095e9e140..f00ec631f 100644 --- a/client/data/help_guide/pt-PT.html +++ b/client/data/help_guide/pt-PT.html @@ -776,6 +776,9 @@

Contas WhatsApp Business

Encomendas criadas pelo vendedor

Enter numa encomenda que um vendedor criou para si abre os dados dela, com artigos e subtotal. Quando a encomenda está pendente e é paga fora da aplicação, "Confirmar informações da encomenda" envia a confirmação ao vendedor, como no WhatsApp Web; a janela diz que respostas do comprador o WinZapp ainda não oferece.

+

Botões nas mensagens de empresas

+

Algumas mensagens de empresas vêm com botões. Selecione a mensagem e prima Tab: os botões são botões a sério, por baixo da lista de mensagens, na ordem do Tab. Um botão de resposta envia a resposta à empresa, da mesma forma que o WhatsApp Web, e o WinZapp diz se foi enviada. As linhas de uma mensagem de lista também são botões, um para cada linha. Um botão de ligação abre a ligação, um botão de copiar copia o código e um botão de chamada diz o número a marcar. Os botões são da própria empresa, por isso os nomes mudam de mensagem para mensagem.

+
@@ -1264,6 +1267,7 @@

📝 20. Registro de atualizações do guia

Cada alpha nova que traz funcionalidades é acrescentada ao guia. Esta lista mostra até que alpha o guia está completo e o que foi acrescentado em cada rodada.

AlphaO que entrou no guia
Ainda não lançadoBotões nas mensagens de empresas: resposta, link, copiar código e número de telefone.
Ainda não lançadoVerificação de atualizações do macOS: causas e etapas distintas, uma nova tentativa limitada para falhas temporárias e tempos de espera do GitHub.
Ainda não lançadoPreservação do pacote de atualização, exclusividade da instalação, resultado da cópia e reabertura segura. Depois de uma cópia incompleta ou não confirmada, o WinZapp permanece fechado e abre instruções de recuperação no Bloco de Notas. Atender, recusar e desligar ligações de qualquer janela (Ctrl+Alt+Shift+Enter e Ctrl+Alt+Shift+Backspace). Categoria Avançadas, letras de atalho nas Configurações, atalhos em árvore com busca, rascunho por conversa, Tentar enviar de novo, controles depois da lista que podem sair do Tab, fila e janelas da transcrição local, transcrição automática por conversa, status que não foram postados e apagar status, grupos de comunidades, pedidos criados pelo vendedor e privacidade do status.
Ainda não lançadoAdaptação automática do áudio das chamadas à taxa de amostragem do microfone e do alto-falante.
+ diff --git a/client/data/help_guide/ro.html b/client/data/help_guide/ro.html index f98f05072..7c01732cc 100644 --- a/client/data/help_guide/ro.html +++ b/client/data/help_guide/ro.html @@ -776,6 +776,9 @@

Conturi WhatsApp Business

Comenzi create de vânzător

Enter pe o comandă creată pentru dumneavoastră de un vânzător deschide datele ei, cu articole și subtotal. Când comanda este în așteptare și se plătește în afara aplicației, "Confirmați informațiile comenzii" trimite confirmarea vânzătorului, ca în WhatsApp Web; fereastra spune ce răspunsuri ale cumpărătorului WinZapp nu oferă încă.

+

Butoane în mesajele firmelor

+

Unele mesaje de la firme vin cu butoane. Selectați mesajul și apăsați Tab: butoanele sunt butoane adevărate, sub lista de mesaje, în ordinea tastei Tab. Un buton de răspuns trimite răspunsul către firmă, la fel ca WhatsApp Web, iar WinZapp spune dacă a fost trimis. Rândurile unui mesaj cu listă sunt și ele butoane, câte unul pentru fiecare rând. Un buton de link deschide linkul, un buton de copiere copiază codul, iar un buton de apel spune numărul de format. Butoanele sunt ale firmei, deci numele lor se schimbă de la un mesaj la altul.

+
@@ -1264,6 +1267,7 @@

📝 20. Jurnalul actualizărilor ghidului

Fiecare alfa nou care aduce funcții este adăugat în ghid. Această listă arată până la ce alfa este complet ghidul și ce a intrat în fiecare rundă.

AlphaO que entrou no guia
Ainda não lançadoBotões nas mensagens de empresas: resposta, ligação, copiar código e número de telefone.
Ainda não lançadoVerificação de atualizações do macOS: causas e etapas distintas, uma nova tentativa limitada para falhas temporárias e tempos de espera do GitHub.
Ainda não lançadoPreservação do pacote de atualização, exclusividade da instalação, resultado da cópia e reabertura segura. Após uma cópia incompleta ou não confirmada, o WinZapp permanece fechado e abre instruções de recuperação no Bloco de Notas. Atender, recusar e terminar chamadas a partir de qualquer janela (Ctrl+Alt+Shift+Enter e Ctrl+Alt+Shift+Backspace). Categoria Avançadas, letras de atalho nas Definições, atalhos em árvore com pesquisa, rascunho por conversa, Tentar enviar de novo, controlos depois da lista que podem sair do Tab, fila e janelas da transcrição local, transcrição automática por conversa, estados que não foram publicados e apagar um estado, grupos de uma comunidade, encomendas criadas pelo vendedor e privacidade do estado.
Ainda não lançadoAdaptação automática do áudio das chamadas à frequência de amostragem do microfone e do altifalante.
+ diff --git a/client/data/help_guide/tr-TR.html b/client/data/help_guide/tr-TR.html index ab430d98b..764e92db2 100644 --- a/client/data/help_guide/tr-TR.html +++ b/client/data/help_guide/tr-TR.html @@ -776,6 +776,9 @@

WhatsApp Business hesapları

Satıcının oluşturduğu siparişler

Satıcının sizin için oluşturduğu bir siparişte Enter, kalemleri ve ara toplamıyla sipariş bilgilerini açar. Sipariş beklemedeyken ve uygulama dışında ödeniyorsa "Sipariş bilgilerini onayla", WhatsApp Web'deki gibi satıcıya onayı gönderir; pencere de alıcının hangi yanıtlarını WinZapp'in henüz sunmadığını söyler.

+

İşletme mesajlarındaki düğmeler

+

İşletmelerden gelen bazı mesajlarda düğmeler bulunur. Mesajı seçin ve Tab tuşuna basın: düğmeler mesaj listesinin altında, Tab sırasında yer alan gerçek düğmelerdir. Yanıt düğmesi yanıtı işletmeye WhatsApp Web ile aynı şekilde gönderir ve WinZapp gönderilip gönderilmediğini söyler. Liste mesajlarının satırları da düğmedir, her satır için bir tane. Bağlantı düğmesi bağlantıyı açar, kopyalama düğmesi kodu kopyalar, arama düğmesi aranacak numarayı söyler. Düğmeler işletmeye aittir; bu yüzden adları mesajdan mesaja değişir.

+
@@ -1264,6 +1267,7 @@

📝 20. Kılavuz değişiklik günlüğü

Özellik getiren her yeni alpha kılavuza eklenir. Bu liste kılavuzun hangi alpha'ya kadar tam olduğunu ve her turda neyin eklendiğini gösterir.

AlfaCe a intrat în ghid
Încă nepublicatButoane în mesajele firmelor: răspuns, link, copiere cod și număr de telefon.
Încă nepublicatVerificarea actualizărilor macOS: cauze și etape distincte, o singură reîncercare limitată pentru erori temporare și timpi de așteptare GitHub.
Încă nepublicatPăstrarea pachetului de actualizare, exclusivitatea instalării, rezultatul copierii și redeschiderea în siguranță. După o copiere incompletă sau neconfirmată, WinZapp rămâne închis și deschide instrucțiuni de reparare în Notepad. Răspunsul, respingerea și încheierea apelurilor din orice fereastră (Ctrl+Alt+Shift+Enter și Ctrl+Alt+Shift+Backspace). Categoria Avansate, litere de scurtătură în Setări, comenzi rapide într-un arbore cu căutare, ciornă pe conversație, Încearcă să trimiți din nou, controale de după listă care pot ieși din ordinea Tab, coada și ferestrele transcrierii locale, transcriere automată pe conversație, stări care nu au fost publicate și ștergerea unei stări, grupurile unei comunități, comenzi create de vânzător și confidențialitatea stării.
Încă nepublicatAdaptarea automată a sunetului apelurilor la rata de eșantionare a microfonului și a difuzorului.
+ diff --git a/client/ui/conversation_panel/__init__.py b/client/ui/conversation_panel/__init__.py index 1178be986..89c3b5495 100644 --- a/client/ui/conversation_panel/__init__.py +++ b/client/ui/conversation_panel/__init__.py @@ -53,6 +53,7 @@ message_search search inside the conversation chat_menu conversations-list context menu and chat actions commerce a business's catalog and cart, an order's window, a business account's tools + business_buttons the buttons of a business's message: reply, link, copy code, phone number chat_list_selection chat multi-selection shared by every chat list (main, archived, locked) chat_selection the conversations list's keys and bulk chat actions conversation_info conversation data, profile, presence note diff --git a/client/ui/conversation_panel/business_buttons.py b/client/ui/conversation_panel/business_buttons.py new file mode 100644 index 000000000..436195b7f --- /dev/null +++ b/client/ui/conversation_panel/business_buttons.py @@ -0,0 +1,82 @@ +"""BusinessButtonsMixin — part of ConversationsPanel (see ui/conversation_panel/__init__.py). + +The buttons of a business's message (core/business_buttons.py): one real +button each, under the message list, in the order of Tab. A reply is sent the +way WhatsApp Web sends it, a link opens, a code is copied and a number is +spoken. +""" + +import threading + +import wx + +from core.business_buttons import CALL, COPY, REPLY, URL, ButtonReplyError, reply_to_button +from core.group_invitation import connection_identity + + +class BusinessButtonsMixin: + def _show_business_buttons(self, msg: dict): + """One button for each button of the selected message, in the + container the legacy reply buttons use.""" + buttons = msg.get("businessButtons") or [] + self._business_sending = False + self._buttons_container.DestroyChildren() + sizer = wx.WrapSizer(wx.HORIZONTAL) + for button in buttons: + control = wx.Button(self._buttons_container, label=button["label"].replace("&", "&&")) + control.Bind(wx.EVT_BUTTON, lambda e, b=button, m=msg: self._on_business_button(b, m)) + sizer.Add(control, 0, wx.ALL, 4) + self._buttons_container.SetSizer(sizer, True) + self._buttons_container.Layout() + self._buttons_container.Show() + self.conversation_panel.Layout() + + def _on_business_button(self, button: dict, msg: dict): + mw = self.main_window + i18n = mw.i18n + kind = button.get("kind") + if kind == URL: + self._activate_link(button["value"]) + elif kind == COPY: + self._copy_business_text(button["value"]) + elif kind == CALL: + mw.output(i18n.t("business_button_call").format(number=button["value"]), interrupt=True) + elif kind == REPLY: + self._send_business_reply(button, msg) + + def _copy_business_text(self, text: str): + mw = self.main_window + if wx.TheClipboard.Open(): + try: + wx.TheClipboard.SetData(wx.TextDataObject(text)) + wx.TheClipboard.Flush() + finally: + wx.TheClipboard.Close() + mw.output(mw.i18n.t("business_button_copied"), interrupt=True) + else: + mw.output(mw.i18n.t("msg_copy_error"), interrupt=True) + + def _send_business_reply(self, button: dict, msg: dict): + """Once per tap: a second tap while the first is on its way is + ignored, and a failure is never retried by itself.""" + if getattr(self, "_business_sending", False): + return + mw = self.main_window + jid = self.conversation.get("remoteJid", "") if self.conversation else "" + message_id = mw._serialize_msg_id(jid, msg.get("key") or {}, msg) + connection = connection_identity(mw) + self._business_sending = True + + def run(): + try: + reply_to_button(connection, message_id, button) + sent = True + except ButtonReplyError: + sent = False + wx.CallAfter(self._business_reply_done, sent) + threading.Thread(target=run, daemon=True).start() + + def _business_reply_done(self, sent: bool): + self._business_sending = False + mw = self.main_window + mw.output(mw.i18n.t("business_button_sent" if sent else "business_button_failed"), interrupt=True) diff --git a/client/ui/conversation_panel/message_list.py b/client/ui/conversation_panel/message_list.py index 58dd3cd0f..a1dfdc6c6 100644 --- a/client/ui/conversation_panel/message_list.py +++ b/client/ui/conversation_panel/message_list.py @@ -179,6 +179,9 @@ def on_message_selected(self, event): self._action_open_btn.Show() self.conversation_panel.Layout() + if msg.get("businessButtons"): + self._show_business_buttons(msg) + self._update_ai_describe_button(msg) if self._saved_media_path(msg): diff --git a/client/ui/conversations.py b/client/ui/conversations.py index 56f1da58d..94c038585 100644 --- a/client/ui/conversations.py +++ b/client/ui/conversations.py @@ -165,6 +165,7 @@ from ui.conversation_panel.message_search import MessageSearchMixin from ui.conversation_panel.community_comments import CommunityCommentsPanelMixin from ui.conversation_panel.commerce import CommercePanelMixin +from ui.conversation_panel.business_buttons import BusinessButtonsMixin from ui.conversation_panel.pinned_messages import PinnedMessagesMixin from ui.conversation_panel.reactions import ReactionsMixin from ui.conversation_panel.attachments import AttachmentsMixin @@ -215,6 +216,7 @@ class ConversationsPanel( MessageSearchMixin, CommunityCommentsPanelMixin, CommercePanelMixin, + BusinessButtonsMixin, PinnedMessagesMixin, ReactionsMixin, AttachmentsMixin, diff --git a/client/ui/dialogs/api_setup.py b/client/ui/dialogs/api_setup.py index e241ce601..c52098a67 100644 --- a/client/ui/dialogs/api_setup.py +++ b/client/ui/dialogs/api_setup.py @@ -199,6 +199,8 @@ "src/tests/controller/pageHeapController.test.ts", "src/controller/orderConfirmController.ts", "src/tests/controller/orderConfirmController.test.ts", + "src/controller/buttonReplyController.ts", + "src/tests/controller/buttonReplyController.test.ts", "src/controller/communityAdminController.ts", "src/tests/controller/communityAdminController.test.ts", "src/controller/groupMembershipRequestsController.ts", diff --git a/docs/user-guide/cobertura.json b/docs/user-guide/cobertura.json index 041771c1a..8a23750a7 100644 --- a/docs/user-guide/cobertura.json +++ b/docs/user-guide/cobertura.json @@ -4,6 +4,7 @@ "commit": "4b8a5c6a", "data": "14/10/2026", "itens": { + "ebe407996699": "Os botões das mensagens de empresas agora podem ser usados: um botão de resposta", "b3db49355a14": "O WPPConnect Server 2.10.61 agora é oferecido como atualização sem esperar por u", "57b296f613e7": "As atualizações do macOS explicam se a verificação falhou por um problema de con", "08bdfe02f5a9": "Atualizações: a instalação mantém a exclusividade depois que o WinZapp fecha, e ", diff --git a/docs/user-guide/secoes/en-US/12-empresas.html b/docs/user-guide/secoes/en-US/12-empresas.html index 330d59734..04fe237ff 100644 --- a/docs/user-guide/secoes/en-US/12-empresas.html +++ b/docs/user-guide/secoes/en-US/12-empresas.html @@ -12,3 +12,6 @@

WhatsApp Business accounts

Orders created by the seller

Enter on an order a seller created for you opens its details, with items and subtotal. When the order is pending and is paid outside the app, "Confirm order info" sends the seller the confirmation, like WhatsApp Web; the dialog says which buyer responses WinZapp does not offer yet.

+ +

Buttons in business messages

+

Some messages from businesses come with buttons. Select the message and press Tab: the buttons are real buttons under the message list, in the order of Tab. A reply button sends the answer to the business, the way WhatsApp Web does, and WinZapp says whether it was sent. The rows of a list message are buttons too, one for each row. A link button opens the link, a copy button copies the code, and a call button says the number to dial. The buttons are the business's own, so their names change from message to message.

diff --git a/docs/user-guide/secoes/en-US/99-registro.html b/docs/user-guide/secoes/en-US/99-registro.html index 207e20931..8f547e932 100644 --- a/docs/user-guide/secoes/en-US/99-registro.html +++ b/docs/user-guide/secoes/en-US/99-registro.html @@ -2,6 +2,7 @@

Every new alpha that brings features is added to the guide. This list shows up to which alpha the guide is complete and what was added in each round.

AlphaKılavuza girenler
Henüz yayımlanmadıİşletme mesajlarındaki düğmeler: yanıt, bağlantı, kod kopyalama ve telefon numarası.
Henüz yayımlanmadımacOS güncelleme doğrulaması: ayrı hata nedenleri ve doğrulama adımları, geçici hatada süreyle sınırlı tek yeniden deneme ve GitHub bekleme süreleri.
Henüz yayımlanmadıGüncelleme paketinin korunması, kurulum sahipliği, kopyalama sonucu ve güvenli yeniden açılış. Eksik veya tamamlandığı doğrulanamayan kopyalamadan sonra WinZapp kapalı kalır ve onarım yönergelerini Not Defteri'nde açar. Aramaları herhangi bir pencereden yanıtlama, reddetme ve kapatma (Ctrl+Alt+Shift+Enter ve Ctrl+Alt+Shift+Geri sil). Gelişmiş kategorisi, Ayarlar'da kısayol harfleri, aramalı ağaçta kısayollar, sohbet başına taslak, Yeniden göndermeyi dene, Tab sırasından çıkabilen listeden sonraki denetimler, yerel dökümün kuyruğu ve pencereleri, sohbet başına otomatik döküm, paylaşılmayan durumlar ve durum silme, bir topluluğun grupları, satıcının oluşturduğu siparişler ve durum gizliliği.
Henüz yayımlanmadıArama sesinin mikrofon ve hoparlörün örnekleme hızına otomatik uyarlanması.
+ diff --git a/docs/user-guide/secoes/es-ES/12-empresas.html b/docs/user-guide/secoes/es-ES/12-empresas.html index c46e6e1e3..abb91c51c 100644 --- a/docs/user-guide/secoes/es-ES/12-empresas.html +++ b/docs/user-guide/secoes/es-ES/12-empresas.html @@ -12,3 +12,6 @@

Cuentas de WhatsApp Business

Pedidos creados por el vendedor

Intro en un pedido que un vendedor creó para ti abre sus datos, con artículos y subtotal. Cuando el pedido está pendiente y se paga fuera de la aplicación, "Confirmar información del pedido" envía la confirmación al vendedor, como en WhatsApp Web; el diálogo dice qué respuestas del comprador WinZapp aún no ofrece.

+ +

Botones en los mensajes de empresas

+

Algunos mensajes de empresas traen botones. Seleccione el mensaje y pulse Tab: los botones son botones de verdad, debajo de la lista de mensajes, en el orden del Tab. Un botón de respuesta envía la respuesta a la empresa, igual que WhatsApp Web, y WinZapp dice si se envió. Las filas de un mensaje de lista también son botones, uno por cada fila. Un botón de enlace abre el enlace, un botón de copiar copia el código y un botón de llamada dice el número que hay que marcar. Los botones son de la propia empresa, así que sus nombres cambian de un mensaje a otro.

diff --git a/docs/user-guide/secoes/es-ES/99-registro.html b/docs/user-guide/secoes/es-ES/99-registro.html index 65967734a..4bb05afd3 100644 --- a/docs/user-guide/secoes/es-ES/99-registro.html +++ b/docs/user-guide/secoes/es-ES/99-registro.html @@ -2,6 +2,7 @@

Cada alpha nueva que trae funciones se añade a la guía. Esta lista muestra hasta qué alpha está completa la guía y qué se añadió en cada ronda.

AlphaWhat went into the guide
Not yet releasedButtons in business messages: reply, link, copy code and phone number.
Not yet releasedmacOS update verification: distinct causes and verification stages, one bounded transient retry, and GitHub wait times.
Not yet releasedUpdate package retention, installation ownership, copy results and safe reopening. After an incomplete or unconfirmed copy, WinZapp stays closed and opens recovery instructions in Notepad. Answering, rejecting and hanging up calls from any window (Ctrl+Alt+Shift+Enter and Ctrl+Alt+Shift+Backspace). Advanced category, shortcut letters in Settings, shortcuts in a tree with search, draft per conversation, Try sending again, controls after the list that can leave the Tab order, the queue and windows of local transcription, automatic transcription per conversation, statuses that were not posted and deleting a status, groups of a community, orders created by the seller and status privacy.
Not yet releasedAutomatic adaptation of call audio to microphone and speaker sample rates.
+ diff --git a/docs/user-guide/secoes/pl/12-empresas.html b/docs/user-guide/secoes/pl/12-empresas.html index b27d45565..0808ed30f 100644 --- a/docs/user-guide/secoes/pl/12-empresas.html +++ b/docs/user-guide/secoes/pl/12-empresas.html @@ -12,3 +12,6 @@

Konta WhatsApp Business

Zamówienia utworzone przez sprzedawcę

Enter na zamówieniu, które sprzedawca utworzył dla Ciebie, otwiera jego dane z pozycjami i sumą częściową. Gdy zamówienie oczekuje i jest opłacane poza aplikacją, "Potwierdź informacje o zamówieniu" wysyła sprzedawcy potwierdzenie, jak w WhatsApp Web; okno mówi, których odpowiedzi kupującego WinZapp jeszcze nie oferuje.

+ +

Przyciski w wiadomościach firm

+

Niektóre wiadomości od firm mają przyciski. Zaznacz wiadomość i naciśnij Tab: przyciski są prawdziwymi przyciskami pod listą wiadomości, w kolejności klawisza Tab. Przycisk odpowiedzi wysyła odpowiedź do firmy tak samo jak WhatsApp Web, a WinZapp mówi, czy została wysłana. Wiersze wiadomości z listą też są przyciskami, po jednym na wiersz. Przycisk linku otwiera link, przycisk kopiowania kopiuje kod, a przycisk połączenia odczytuje numer do wybrania. Przyciski pochodzą od firmy, więc ich nazwy zmieniają się z wiadomości na wiadomość.

diff --git a/docs/user-guide/secoes/pl/99-registro.html b/docs/user-guide/secoes/pl/99-registro.html index 6fbff51f2..53786268a 100644 --- a/docs/user-guide/secoes/pl/99-registro.html +++ b/docs/user-guide/secoes/pl/99-registro.html @@ -2,6 +2,7 @@

Każda nowa alpha, która wnosi funkcje, jest dodawana do przewodnika. Ta lista pokazuje, do której alphy przewodnik jest kompletny i co dodano w każdej rundzie.

AlphaQué entró en la guía
Aún no publicadoBotones en los mensajes de empresas: respuesta, enlace, copiar código y número de teléfono.
Aún no publicadoVerificación de actualizaciones de macOS: causas y etapas diferenciadas, un reintento limitado para fallos temporales y tiempos de espera de GitHub.
Aún no publicadoConservación del paquete de actualización, exclusividad de la instalación, resultado de la copia y reapertura segura. Tras una copia incompleta o no confirmada, WinZapp permanece cerrado y abre instrucciones de recuperación en el Bloc de notas. Contestar, rechazar y colgar llamadas desde cualquier ventana (Ctrl+Alt+Shift+Enter y Ctrl+Alt+Shift+Retroceso). Categoría Avanzadas, letras de atajo en la Configuración, atajos en un árbol con búsqueda, borrador por conversación, Intentar enviar de nuevo, controles después de la lista que pueden salir del Tab, cola y ventanas de la transcripción local, transcripción automática por conversación, estados que no se publicaron y eliminar un estado, grupos de una comunidad, pedidos creados por el vendedor y privacidad del estado.
Aún no publicadoAdaptación automática del audio de las llamadas a la frecuencia de muestreo del micrófono y del altavoz.
+ diff --git a/docs/user-guide/secoes/pt-BR/12-empresas.html b/docs/user-guide/secoes/pt-BR/12-empresas.html index 9c874b9aa..af76ba67b 100644 --- a/docs/user-guide/secoes/pt-BR/12-empresas.html +++ b/docs/user-guide/secoes/pt-BR/12-empresas.html @@ -12,3 +12,6 @@

Contas WhatsApp Business

Pedidos criados pelo vendedor

Enter num pedido que um vendedor criou para você abre os dados dele, com itens e subtotal. Quando o pedido está pendente e é pago fora do aplicativo, "Confirmar informações do pedido" envia a confirmação ao vendedor, como no WhatsApp Web; o diálogo diz quais respostas do comprador o WinZapp ainda não oferece.

+ +

Botões nas mensagens de empresas

+

Algumas mensagens de empresas vêm com botões. Selecione a mensagem e aperte Tab: os botões são botões de verdade, abaixo da lista de mensagens, na ordem do Tab. Um botão de resposta envia a resposta à empresa, do mesmo jeito que o WhatsApp Web, e o WinZapp diz se ela foi enviada. As linhas de uma mensagem de lista também são botões, um para cada linha. Um botão de link abre o link, um botão de copiar copia o código e um botão de ligar diz o número para discar. Os botões são da própria empresa, então os nomes mudam de mensagem para mensagem.

diff --git a/docs/user-guide/secoes/pt-BR/99-registro.html b/docs/user-guide/secoes/pt-BR/99-registro.html index e3359a31d..ecb698f2b 100644 --- a/docs/user-guide/secoes/pt-BR/99-registro.html +++ b/docs/user-guide/secoes/pt-BR/99-registro.html @@ -2,6 +2,7 @@

Cada alpha nova que traz funcionalidades é acrescentada ao guia. Esta lista mostra até que alpha o guia está completo e o que foi acrescentado em cada rodada.

AlphaCo trafiło do przewodnika
Jeszcze niewydanePrzyciski w wiadomościach firm: odpowiedź, link, kopiowanie kodu i numer telefonu.
Jeszcze niewydaneWeryfikacja aktualizacji macOS: osobne przyczyny i etapy, jedna ograniczona ponowna próba przy błędach tymczasowych i czas oczekiwania GitHub.
Jeszcze niewydaneZachowywanie pakietu aktualizacji, wyłączność instalacji, wynik kopiowania i bezpieczne ponowne otwarcie. Po niepełnym lub niepotwierdzonym kopiowaniu WinZapp pozostaje zamknięty i otwiera instrukcje naprawy w Notatniku. Odbieranie, odrzucanie i rozłączanie połączeń z dowolnego okna (Ctrl+Alt+Shift+Enter i Ctrl+Alt+Shift+Backspace). Kategoria Zaawansowane, litery skrótów w Ustawieniach, skróty w drzewie z wyszukiwaniem, wersja robocza dla każdego czatu, Spróbuj wysłać ponownie, elementy za listą, które mogą wyjść z kolejności klawisza Tab, kolejka i okna lokalnej transkrypcji, automatyczna transkrypcja dla każdego czatu, statusy, które nie zostały opublikowane, i usuwanie statusu, grupy społeczności, zamówienia utworzone przez sprzedawcę i prywatność statusu.
Jeszcze niewydaneAutomatyczne dostosowanie dźwięku połączeń do częstotliwości próbkowania mikrofonu i głośnika.
+ diff --git a/docs/user-guide/secoes/pt-PT/12-empresas.html b/docs/user-guide/secoes/pt-PT/12-empresas.html index 43499642b..7e8957bba 100644 --- a/docs/user-guide/secoes/pt-PT/12-empresas.html +++ b/docs/user-guide/secoes/pt-PT/12-empresas.html @@ -12,3 +12,6 @@

Contas WhatsApp Business

Encomendas criadas pelo vendedor

Enter numa encomenda que um vendedor criou para si abre os dados dela, com artigos e subtotal. Quando a encomenda está pendente e é paga fora da aplicação, "Confirmar informações da encomenda" envia a confirmação ao vendedor, como no WhatsApp Web; a janela diz que respostas do comprador o WinZapp ainda não oferece.

+ +

Botões nas mensagens de empresas

+

Algumas mensagens de empresas vêm com botões. Selecione a mensagem e prima Tab: os botões são botões a sério, por baixo da lista de mensagens, na ordem do Tab. Um botão de resposta envia a resposta à empresa, da mesma forma que o WhatsApp Web, e o WinZapp diz se foi enviada. As linhas de uma mensagem de lista também são botões, um para cada linha. Um botão de ligação abre a ligação, um botão de copiar copia o código e um botão de chamada diz o número a marcar. Os botões são da própria empresa, por isso os nomes mudam de mensagem para mensagem.

diff --git a/docs/user-guide/secoes/pt-PT/99-registro.html b/docs/user-guide/secoes/pt-PT/99-registro.html index 5d83c2a82..86cb44f84 100644 --- a/docs/user-guide/secoes/pt-PT/99-registro.html +++ b/docs/user-guide/secoes/pt-PT/99-registro.html @@ -2,6 +2,7 @@

Cada alpha nova que traz funcionalidades é acrescentada ao guia. Esta lista mostra até que alpha o guia está completo e o que foi acrescentado em cada rodada.

AlphaO que entrou no guia
Ainda não lançadoBotões nas mensagens de empresas: resposta, link, copiar código e número de telefone.
Ainda não lançadoVerificação de atualizações do macOS: causas e etapas distintas, uma nova tentativa limitada para falhas temporárias e tempos de espera do GitHub.
Ainda não lançadoPreservação do pacote de atualização, exclusividade da instalação, resultado da cópia e reabertura segura. Depois de uma cópia incompleta ou não confirmada, o WinZapp permanece fechado e abre instruções de recuperação no Bloco de Notas. Atender, recusar e desligar ligações de qualquer janela (Ctrl+Alt+Shift+Enter e Ctrl+Alt+Shift+Backspace). Categoria Avançadas, letras de atalho nas Configurações, atalhos em árvore com busca, rascunho por conversa, Tentar enviar de novo, controles depois da lista que podem sair do Tab, fila e janelas da transcrição local, transcrição automática por conversa, status que não foram postados e apagar status, grupos de comunidades, pedidos criados pelo vendedor e privacidade do status.
Ainda não lançadoAdaptação automática do áudio das chamadas à taxa de amostragem do microfone e do alto-falante.
+ diff --git a/docs/user-guide/secoes/ro/12-empresas.html b/docs/user-guide/secoes/ro/12-empresas.html index 76464c0e4..568094f7c 100644 --- a/docs/user-guide/secoes/ro/12-empresas.html +++ b/docs/user-guide/secoes/ro/12-empresas.html @@ -12,3 +12,6 @@

Conturi WhatsApp Business

Comenzi create de vânzător

Enter pe o comandă creată pentru dumneavoastră de un vânzător deschide datele ei, cu articole și subtotal. Când comanda este în așteptare și se plătește în afara aplicației, "Confirmați informațiile comenzii" trimite confirmarea vânzătorului, ca în WhatsApp Web; fereastra spune ce răspunsuri ale cumpărătorului WinZapp nu oferă încă.

+ +

Butoane în mesajele firmelor

+

Unele mesaje de la firme vin cu butoane. Selectați mesajul și apăsați Tab: butoanele sunt butoane adevărate, sub lista de mesaje, în ordinea tastei Tab. Un buton de răspuns trimite răspunsul către firmă, la fel ca WhatsApp Web, iar WinZapp spune dacă a fost trimis. Rândurile unui mesaj cu listă sunt și ele butoane, câte unul pentru fiecare rând. Un buton de link deschide linkul, un buton de copiere copiază codul, iar un buton de apel spune numărul de format. Butoanele sunt ale firmei, deci numele lor se schimbă de la un mesaj la altul.

diff --git a/docs/user-guide/secoes/ro/99-registro.html b/docs/user-guide/secoes/ro/99-registro.html index 5b488308f..3204dc345 100644 --- a/docs/user-guide/secoes/ro/99-registro.html +++ b/docs/user-guide/secoes/ro/99-registro.html @@ -2,6 +2,7 @@

Fiecare alfa nou care aduce funcții este adăugat în ghid. Această listă arată până la ce alfa este complet ghidul și ce a intrat în fiecare rundă.

AlphaO que entrou no guia
Ainda não lançadoBotões nas mensagens de empresas: resposta, ligação, copiar código e número de telefone.
Ainda não lançadoVerificação de atualizações do macOS: causas e etapas distintas, uma nova tentativa limitada para falhas temporárias e tempos de espera do GitHub.
Ainda não lançadoPreservação do pacote de atualização, exclusividade da instalação, resultado da cópia e reabertura segura. Após uma cópia incompleta ou não confirmada, o WinZapp permanece fechado e abre instruções de recuperação no Bloco de Notas. Atender, recusar e terminar chamadas a partir de qualquer janela (Ctrl+Alt+Shift+Enter e Ctrl+Alt+Shift+Backspace). Categoria Avançadas, letras de atalho nas Definições, atalhos em árvore com pesquisa, rascunho por conversa, Tentar enviar de novo, controlos depois da lista que podem sair do Tab, fila e janelas da transcrição local, transcrição automática por conversa, estados que não foram publicados e apagar um estado, grupos de uma comunidade, encomendas criadas pelo vendedor e privacidade do estado.
Ainda não lançadoAdaptação automática do áudio das chamadas à frequência de amostragem do microfone e do altifalante.
+ diff --git a/docs/user-guide/secoes/tr-TR/12-empresas.html b/docs/user-guide/secoes/tr-TR/12-empresas.html index b475e6cc2..bf3017470 100644 --- a/docs/user-guide/secoes/tr-TR/12-empresas.html +++ b/docs/user-guide/secoes/tr-TR/12-empresas.html @@ -12,3 +12,6 @@

WhatsApp Business hesapları

Satıcının oluşturduğu siparişler

Satıcının sizin için oluşturduğu bir siparişte Enter, kalemleri ve ara toplamıyla sipariş bilgilerini açar. Sipariş beklemedeyken ve uygulama dışında ödeniyorsa "Sipariş bilgilerini onayla", WhatsApp Web'deki gibi satıcıya onayı gönderir; pencere de alıcının hangi yanıtlarını WinZapp'in henüz sunmadığını söyler.

+ +

İşletme mesajlarındaki düğmeler

+

İşletmelerden gelen bazı mesajlarda düğmeler bulunur. Mesajı seçin ve Tab tuşuna basın: düğmeler mesaj listesinin altında, Tab sırasında yer alan gerçek düğmelerdir. Yanıt düğmesi yanıtı işletmeye WhatsApp Web ile aynı şekilde gönderir ve WinZapp gönderilip gönderilmediğini söyler. Liste mesajlarının satırları da düğmedir, her satır için bir tane. Bağlantı düğmesi bağlantıyı açar, kopyalama düğmesi kodu kopyalar, arama düğmesi aranacak numarayı söyler. Düğmeler işletmeye aittir; bu yüzden adları mesajdan mesaja değişir.

diff --git a/docs/user-guide/secoes/tr-TR/99-registro.html b/docs/user-guide/secoes/tr-TR/99-registro.html index f22ce3ddb..0f32caaa0 100644 --- a/docs/user-guide/secoes/tr-TR/99-registro.html +++ b/docs/user-guide/secoes/tr-TR/99-registro.html @@ -2,6 +2,7 @@

Özellik getiren her yeni alpha kılavuza eklenir. Bu liste kılavuzun hangi alpha'ya kadar tam olduğunu ve her turda neyin eklendiğini gösterir.

AlfaCe a intrat în ghid
Încă nepublicatButoane în mesajele firmelor: răspuns, link, copiere cod și număr de telefon.
Încă nepublicatVerificarea actualizărilor macOS: cauze și etape distincte, o singură reîncercare limitată pentru erori temporare și timpi de așteptare GitHub.
Încă nepublicatPăstrarea pachetului de actualizare, exclusivitatea instalării, rezultatul copierii și redeschiderea în siguranță. După o copiere incompletă sau neconfirmată, WinZapp rămâne închis și deschide instrucțiuni de reparare în Notepad. Răspunsul, respingerea și încheierea apelurilor din orice fereastră (Ctrl+Alt+Shift+Enter și Ctrl+Alt+Shift+Backspace). Categoria Avansate, litere de scurtătură în Setări, comenzi rapide într-un arbore cu căutare, ciornă pe conversație, Încearcă să trimiți din nou, controale de după listă care pot ieși din ordinea Tab, coada și ferestrele transcrierii locale, transcriere automată pe conversație, stări care nu au fost publicate și ștergerea unei stări, grupurile unei comunități, comenzi create de vânzător și confidențialitatea stării.
Încă nepublicatAdaptarea automată a sunetului apelurilor la rata de eșantionare a microfonului și a difuzorului.
+ diff --git a/setup_api.py b/setup_api.py index 2a70112d4..0b566717f 100644 --- a/setup_api.py +++ b/setup_api.py @@ -137,6 +137,8 @@ def _homologated_major() -> str: "src/tests/controller/pageHeapController.test.ts", "src/controller/orderConfirmController.ts", "src/tests/controller/orderConfirmController.test.ts", + "src/controller/buttonReplyController.ts", + "src/tests/controller/buttonReplyController.test.ts", "src/controller/communityAdminController.ts", "src/tests/controller/communityAdminController.test.ts", "src/controller/groupMembershipRequestsController.ts", diff --git a/tests/test_api_patches_in_sync.py b/tests/test_api_patches_in_sync.py index d22b18738..4422084e4 100644 --- a/tests/test_api_patches_in_sync.py +++ b/tests/test_api_patches_in_sync.py @@ -79,6 +79,8 @@ def _setup_api_module(): "src/tests/controller/pageHeapController.test.ts", "src/controller/orderConfirmController.ts", "src/tests/controller/orderConfirmController.test.ts", + "src/controller/buttonReplyController.ts", + "src/tests/controller/buttonReplyController.test.ts", "src/controller/communityAdminController.ts", "src/tests/controller/communityAdminController.test.ts", "src/controller/groupMembershipRequestsController.ts", diff --git a/tests/test_business_buttons.py b/tests/test_business_buttons.py new file mode 100644 index 000000000..51e7651d9 --- /dev/null +++ b/tests/test_business_buttons.py @@ -0,0 +1,207 @@ +"""The buttons of a business's message (core/business_buttons.py, +ui/conversation_panel/business_buttons.py, api_patches buttonReplyController.ts). + +What must hold: each shape of button is read off the message the server +serialized, a link or a code or a number is never answered through the server, +a reply is one request that is never repeated, and a second tap while the first +is on its way sends nothing. +""" + +import json +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from core import business_buttons as bb +from core.business_buttons import ButtonReplyError, business_buttons, reply_to_button +from core.websocket_client import WebSocketClient +from ui.conversation_panel.business_buttons import BusinessButtonsMixin + +CONNECTION = ("http://localhost", 6300, "synthetic-token") +HEADERS = {"Authorization": "Bearer synthetic-token"} +MESSAGE = "false_777@lid_3EB0AAAAAAAAAAAAAAAA" +URL = "http://localhost:6300/api/synthetic-token/reply-button/false_777%40lid_3EB0AAAAAAAAAAAAAAAA" + + +def answer(body, status=200): + return SimpleNamespace(status_code=status, json=lambda: body) + + +def native(name, **params): + return {"name": name, "buttonParamsJson": json.dumps(params)} + + +class TestReadingTheButtons: + def test_reply_buttons_of_a_plain_message_carry_their_id(self): + msg = {"dynamicReplyButtons": [ + {"buttonId": "1", "buttonText": {"displayText": "Car"}, "type": 1}, + {"buttonId": "2", "buttonText": {"displayText": "Home"}, "type": 1}]} + assert business_buttons(msg) == [ + {"kind": "reply", "source": "dynamic", "index": 0, "label": "Car", "value": "1"}, + {"kind": "reply", "source": "dynamic", "index": 1, "label": "Home", "value": "2"}] + + def test_an_interactive_message_gives_each_kind_its_own_value(self): + msg = {"interactivePayload": {"buttons": [ + native("quick_reply", display_text="Debt", id="d1"), + native("cta_url", display_text="Site", url="https://x.com/a"), + native("cta_copy", display_text="Code", copy_code="ABC"), + native("cta_call", display_text="Call", phone_number="+5511900000001")]}} + assert [(b["kind"], b["index"], b["label"], b["value"]) for b in business_buttons(msg)] == [ + ("reply", 0, "Debt", "d1"), ("url", 1, "Site", "https://x.com/a"), + ("copy", 2, "Code", "ABC"), ("call", 3, "Call", "+5511900000001")] + + def test_the_index_is_the_place_in_the_original_list(self): + msg = {"interactivePayload": {"buttons": [ + native("cta_catalog", display_text="Catalog"), native("quick_reply", display_text="Yes", id="y")]}} + assert [(b["index"], b["source"]) for b in business_buttons(msg)] == [(1, "native")] + + def test_the_rows_of_a_list_are_numbered_across_its_sections(self): + msg = {"list": {"buttonText": "Menu", "sections": [ + {"rows": [{"title": "Outage", "rowId": "r1"}, {"title": " ", "rowId": "skipped"}]}, + {"rows": [{"title": "Bills", "rowId": "r3", "description": "x"}]}]}} + assert [(b["source"], b["index"], b["label"], b["value"]) for b in business_buttons(msg)] == [ + ("list", 0, "Outage", "r1"), ("list", 2, "Bills", "r3")] + + def test_template_buttons_are_answered_by_index(self): + msg = {"hydratedButtons": [{"quickReplyButton": {"id": "q", "displayText": "Yes"}}, + {"urlButton": {"displayText": "Site", "url": "https://x.com"}}, + {"callButton": {"displayText": "Call", "phoneNumber": "123"}}]} + assert [(b["kind"], b["source"], b["index"]) for b in business_buttons(msg)] == [ + ("reply", "template", 0), ("url", "template", 1), ("call", "template", 2)] + + @pytest.mark.parametrize("msg", [ + {}, None, "text", {"dynamicReplyButtons": "x"}, + {"dynamicReplyButtons": [{"buttonId": "1", "buttonText": {"displayText": " "}}]}, + {"dynamicReplyButtons": [{"buttonText": {"displayText": "No id"}}]}, + {"interactivePayload": {"buttons": [native("quick_reply", display_text="Used", id="u", disabled=True)]}}, + {"interactivePayload": {"buttons": [native("cta_url", display_text="Bad", url="javascript:alert(1)")]}}, + {"interactivePayload": {"buttons": [{"name": "quick_reply", "buttonParamsJson": "not json"}]}}, + {"interactivePayload": {"buttons": [native("cta_copy", display_text="Empty", copy_code="")]}}, + {"hydratedButtons": [{"urlButton": {"displayText": "Bad", "url": "ftp://x"}}]}, + ]) + def test_what_cannot_be_acted_on_is_not_offered(self, msg): + assert business_buttons(msg) == [] + + def test_a_long_label_is_cut_and_the_number_of_buttons_is_limited(self): + msg = {"dynamicReplyButtons": [ + {"buttonId": str(i), "buttonText": {"displayText": "x" * 200}} for i in range(30)]} + found = business_buttons(msg) + assert len(found) == bb.MAX_BUTTONS and len(found[0]["label"]) == bb.LABEL_LIMIT + + +class _Normalizer: + _normalize_wpp_message = WebSocketClient._normalize_wpp_message + _clean_jid = WebSocketClient._clean_jid + + +def test_the_buttons_are_kept_with_the_message_and_only_when_there_are_some(): + raw = {"id": "false_777@lid_3EB0BBBBBBBBBBBBBBBB", "type": "chat", "t": 1790000200, "from": "777@lid", + "to": "999@lid", "fromMe": False, "body": "Home or car?", + "dynamicReplyButtons": [{"buttonId": "2", "buttonText": {"displayText": "Home"}}], + "chatId": {"server": "lid", "user": "777", "_serialized": "777@lid"}} + kept = _Normalizer()._normalize_wpp_message(raw) + assert kept["businessButtons"] == [ + {"kind": "reply", "source": "dynamic", "index": 0, "label": "Home", "value": "2"}] + del raw["dynamicReplyButtons"] + assert "businessButtons" not in _Normalizer()._normalize_wpp_message(raw) + + +class TestTheRequest: + BUTTON = {"kind": "reply", "source": "native", "index": 2, "label": "Yes", "value": "y"} + + def test_a_reply_is_one_post_with_the_source_and_the_index(self, monkeypatch): + post = Mock(return_value=answer({"status": "success", "response": {"sent": True}})) + monkeypatch.setattr(bb, "api_post", post) + reply_to_button(CONNECTION, MESSAGE, self.BUTTON) + post.assert_called_once_with(URL, json={"source": "native", "index": 2}, headers=HEADERS, timeout=45) + + @pytest.mark.parametrize("body, status, code", [ + ({"status": "error", "code": "button_used"}, 409, "button_used"), + ({"status": "error", "code": "reply_button_unavailable"}, 503, "unavailable"), + ({"status": "error", "code": 5}, 500, "unavailable"), + ("nothing", 200, "unavailable"), + ]) + def test_a_refusal_keeps_the_servers_code_and_is_not_retried(self, monkeypatch, body, status, code): + post = Mock(return_value=answer(body, status)) + monkeypatch.setattr(bb, "api_post", post) + with pytest.raises(ButtonReplyError) as error: + reply_to_button(CONNECTION, MESSAGE, self.BUTTON) + assert error.value.code == code and post.call_count == 1 + + def test_a_failed_connection_is_unavailable(self, monkeypatch): + monkeypatch.setattr(bb, "api_post", Mock(side_effect=ConnectionError("down"))) + with pytest.raises(ButtonReplyError) as error: + reply_to_button(CONNECTION, MESSAGE, self.BUTTON) + assert error.value.code == "unavailable" + + @pytest.mark.parametrize("message, button", [ + ("../x", BUTTON), ("false_777@g.us_3EB0AAAAAAAAAAAA", BUTTON), (None, BUTTON), + (MESSAGE, {**BUTTON, "kind": "url"}), (MESSAGE, {**BUTTON, "source": "other"}), (MESSAGE, None), + ]) + def test_nothing_is_sent_for_what_is_not_a_reply_to_a_message(self, monkeypatch, message, button): + post = Mock() + monkeypatch.setattr(bb, "api_post", post) + with pytest.raises(ButtonReplyError): + reply_to_button(CONNECTION, message, button) + post.assert_not_called() + + +class _Panel(BusinessButtonsMixin): + """The mixin on a plain stub: no window.""" + + def __init__(self): + self.said = [] + self.main_window = SimpleNamespace( + i18n=SimpleNamespace(t=lambda key: key), + output=lambda text, interrupt=False: self.said.append(text), + _serialize_msg_id=lambda jid, key, msg: MESSAGE, + wpp_server="http://localhost", wpp_port=6300, token="synthetic-token") + self.conversation = {"remoteJid": "777@lid"} + self.opened = [] + self._activate_link = self.opened.append + + +MSG = {"key": {"id": "3EB0AAAAAAAAAAAAAAAA"}} + + +def test_a_link_opens_and_a_number_is_spoken_without_asking_the_server(monkeypatch): + post = Mock() + monkeypatch.setattr(bb, "api_post", post) + panel = _Panel() + panel._on_business_button({"kind": "url", "value": "https://x.com"}, MSG) + panel._on_business_button({"kind": "call", "value": "123"}, MSG) + assert panel.opened == ["https://x.com"] and panel.said == ["business_button_call"] + post.assert_not_called() + + +def test_a_reply_is_sent_once_and_a_second_tap_meanwhile_sends_nothing(monkeypatch): + sent = [] + monkeypatch.setattr("ui.conversation_panel.business_buttons.reply_to_button", + lambda connection, message_id, button: sent.append((connection, message_id, button))) + done = [] + monkeypatch.setattr("ui.conversation_panel.business_buttons.wx.CallAfter", lambda fn, *a: done.append((fn, a))) + monkeypatch.setattr("ui.conversation_panel.business_buttons.threading.Thread", + lambda target, daemon: SimpleNamespace(start=target)) + panel = _Panel() + button = {"kind": "reply", "source": "dynamic", "index": 1, "label": "Home", "value": "2"} + panel._on_business_button(button, MSG) + panel._on_business_button(button, MSG) + assert sent == [(("http://localhost", 6300, "synthetic-token"), MESSAGE, button)] + fn, args = done[0] + fn(*args) + assert panel.said == ["business_button_sent"] + panel._on_business_button(button, MSG) # answered: a new tap is a new reply + assert len(sent) == 2 + + +def test_a_reply_that_failed_says_so(monkeypatch): + def refuse(connection, message_id, button): + raise ButtonReplyError("unavailable") + monkeypatch.setattr("ui.conversation_panel.business_buttons.reply_to_button", refuse) + monkeypatch.setattr("ui.conversation_panel.business_buttons.wx.CallAfter", lambda fn, *a: fn(*a)) + monkeypatch.setattr("ui.conversation_panel.business_buttons.threading.Thread", + lambda target, daemon: SimpleNamespace(start=target)) + panel = _Panel() + panel._on_business_button({"kind": "reply", "source": "native", "index": 0, "label": "Yes", "value": "y"}, MSG) + assert panel.said == ["business_button_failed"] diff --git a/translations/en-US/LC_MESSAGES/winzapp.po b/translations/en-US/LC_MESSAGES/winzapp.po index a82998e5a..f9c4492f4 100644 --- a/translations/en-US/LC_MESSAGES/winzapp.po +++ b/translations/en-US/LC_MESSAGES/winzapp.po @@ -2290,6 +2290,31 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Bulk transcription" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "To call, dial {number}." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Copied." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" +"The reply could not be sent. Check the conversation before trying again." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Reply sent." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7391,6 +7416,7 @@ msgstr "Message copied." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/es-ES/LC_MESSAGES/winzapp.po b/translations/es-ES/LC_MESSAGES/winzapp.po index 5d8ef1281..18156bc35 100644 --- a/translations/es-ES/LC_MESSAGES/winzapp.po +++ b/translations/es-ES/LC_MESSAGES/winzapp.po @@ -2318,6 +2318,32 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Transcripción en masa" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "Para llamar, marque {number}." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Copiado." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" +"No se pudo enviar la respuesta. Revise la conversación antes de volver a " +"intentarlo." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Respuesta enviada." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7477,6 +7503,7 @@ msgstr "Mensaje copiado." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/pl/LC_MESSAGES/winzapp.po b/translations/pl/LC_MESSAGES/winzapp.po index a5359a8c9..b03f58a4a 100644 --- a/translations/pl/LC_MESSAGES/winzapp.po +++ b/translations/pl/LC_MESSAGES/winzapp.po @@ -2334,6 +2334,31 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Transkrypcja masowa" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "Aby zadzwonić, wybierz numer {number}." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Skopiowano." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" +"Nie udało się wysłać odpowiedzi. Sprawdź czat, zanim spróbujesz ponownie." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Odpowiedź wysłana." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7479,6 +7504,7 @@ msgstr "Wiadomość skopiowana." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/pt-BR/LC_MESSAGES/winzapp.po b/translations/pt-BR/LC_MESSAGES/winzapp.po index 71a72ed2d..fc201f142 100644 --- a/translations/pt-BR/LC_MESSAGES/winzapp.po +++ b/translations/pt-BR/LC_MESSAGES/winzapp.po @@ -2316,6 +2316,32 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Transcrição em massa" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "Para ligar, disque {number}." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Copiado." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" +"Não foi possível enviar a resposta. Confira a conversa antes de tentar de " +"novo." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Resposta enviada." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7463,6 +7489,7 @@ msgstr "Mensagem copiada." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/pt-PT/LC_MESSAGES/winzapp.po b/translations/pt-PT/LC_MESSAGES/winzapp.po index 5934cda7c..a0c578782 100644 --- a/translations/pt-PT/LC_MESSAGES/winzapp.po +++ b/translations/pt-PT/LC_MESSAGES/winzapp.po @@ -2330,6 +2330,32 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Transcrição em massa" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "Para ligar, marque {number}." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Copiado." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" +"Não foi possível enviar a resposta. Verifique a conversa antes de tentar " +"novamente." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Resposta enviada." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7492,6 +7518,7 @@ msgstr "Mensagem copiada." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/ro/LC_MESSAGES/winzapp.po b/translations/ro/LC_MESSAGES/winzapp.po index 44aefde96..7343224ee 100644 --- a/translations/ro/LC_MESSAGES/winzapp.po +++ b/translations/ro/LC_MESSAGES/winzapp.po @@ -2307,6 +2307,32 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Transcriere în bloc" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "Pentru a suna, formați {number}." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Copiat." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" +"Răspunsul nu a putut fi trimis. Verificați conversația înainte de a încerca " +"din nou." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Răspuns trimis." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7468,6 +7494,7 @@ msgstr "Mesaj copiat." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/tr-TR/LC_MESSAGES/winzapp.po b/translations/tr-TR/LC_MESSAGES/winzapp.po index 07e6bd919..bb271ace3 100644 --- a/translations/tr-TR/LC_MESSAGES/winzapp.po +++ b/translations/tr-TR/LC_MESSAGES/winzapp.po @@ -2299,6 +2299,30 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "Toplu yazıya döküm" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "Aramak için {number} numarasını çevirin." + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "Kopyalandı." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "Yanıt gönderilemedi. Tekrar denemeden önce sohbeti kontrol edin." + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "Yanıt gönderildi." + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -7403,6 +7427,7 @@ msgstr "Mesaj kopyalandı." #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py diff --git a/translations/winzapp.pot b/translations/winzapp.pot index b66adcfb7..d5f33c5b1 100644 --- a/translations/winzapp.pot +++ b/translations/winzapp.pot @@ -2100,6 +2100,30 @@ msgctxt "bulk_transcription_window_title" msgid "Bulk transcription" msgstr "" +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +#, python-brace-format +msgctxt "business_button_call" +msgid "To call, dial {number}." +msgstr "" + +#: translations/en-US/LC_MESSAGES/winzapp.po +#: client/ui/conversation_panel/business_buttons.py +msgctxt "business_button_copied" +msgid "Copied." +msgstr "" + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_failed" +msgid "" +"The reply could not be sent. Check the conversation before trying again." +msgstr "" + +#: translations/en-US/LC_MESSAGES/winzapp.po +msgctxt "business_button_sent" +msgid "Reply sent." +msgstr "" + #: translations/en-US/LC_MESSAGES/winzapp.po msgctxt "button_no" msgid "&No" @@ -6989,6 +7013,7 @@ msgstr "" #: translations/en-US/LC_MESSAGES/winzapp.po #: client/ui/conversation_panel/bulk_messages.py +#: client/ui/conversation_panel/business_buttons.py #: client/ui/conversation_panel/contact_messages.py #: client/ui/conversation_panel/links.py #: client/ui/conversation_panel/message_menu.py
AlphaKılavuza girenler
Henüz yayımlanmadıİşletme mesajlarındaki düğmeler: yanıt, bağlantı, kod kopyalama ve telefon numarası.
Henüz yayımlanmadımacOS güncelleme doğrulaması: ayrı hata nedenleri ve doğrulama adımları, geçici hatada süreyle sınırlı tek yeniden deneme ve GitHub bekleme süreleri.
Henüz yayımlanmadıGüncelleme paketinin korunması, kurulum sahipliği, kopyalama sonucu ve güvenli yeniden açılış. Eksik veya tamamlandığı doğrulanamayan kopyalamadan sonra WinZapp kapalı kalır ve onarım yönergelerini Not Defteri'nde açar. Aramaları herhangi bir pencereden yanıtlama, reddetme ve kapatma (Ctrl+Alt+Shift+Enter ve Ctrl+Alt+Shift+Geri sil). Gelişmiş kategorisi, Ayarlar'da kısayol harfleri, aramalı ağaçta kısayollar, sohbet başına taslak, Yeniden göndermeyi dene, Tab sırasından çıkabilen listeden sonraki denetimler, yerel dökümün kuyruğu ve pencereleri, sohbet başına otomatik döküm, paylaşılmayan durumlar ve durum silme, bir topluluğun grupları, satıcının oluşturduğu siparişler ve durum gizliliği.
Henüz yayımlanmadıArama sesinin mikrofon ve hoparlörün örnekleme hızına otomatik uyarlanması.