Skip to content

Commit

Permalink
feat(channel-web): delete conversation (#4289)
Browse files Browse the repository at this point in the history
* Draft

* Typos and comments

* Draft

* clearConversation clears current conversation instead of messages only

* Typos + Linting

* PR comments

* Removed incorrect where clause

* Linting

* Improved the way webchat history is cleared

* Only delete events related to handoff

* changed config param from allowHandoffDeletion -> enableHandoffDeletion

* Allow to delete resolved handoff

* Changed delete handoff for delete conversation

* Removed unused reducer

* Only delete the conversation messages

* Renamed deleteMessages to deleteMessagesInChannelWeb

* Linting

* feat(channel-web): delete conversation button (#4303)

* Draft

* Added ConfirmDialog component into ui-shared-lite

* Added transalations

* Added conversation deletion from channel-web

* Linting

* Linting

* Removed confirmDialog from ui-shared

* Changed button behaviour to only delete conversation

* Try importing blueprint css in channel-web

* Typo

* Duplicated confirmDialog. Lite version requires decline btn and  labels

* Fix

* Comment describing ConfirmDialog duplication + removed unecessary Dialog
  • Loading branch information
laurentlp committed Dec 17, 2020
1 parent 65e7d66 commit 742e28e
Show file tree
Hide file tree
Showing 32 changed files with 741 additions and 13 deletions.
5 changes: 5 additions & 0 deletions modules/channel-web/assets/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
--main-bg-color-hover: rgba(255, 255, 255, 0.9);
--main-fg-color: #000000;
--main-fg-color-hover: rgba(0, 0, 0, 0.07);

--seashell: #e2e2e2;
--lighthouse: #f66f48;
--focus-lighthouse: #e06542;
--hover-lighthouse: #f39c82;
}

body {
Expand Down
14 changes: 14 additions & 0 deletions modules/channel-web/src/backend/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,18 @@ export default async (bp: typeof sdk, db: Database) => {

res.send({ txt, name: `${conversation.title}.txt` })
})

router.post('/conversations/:userId/:conversationId/messages/delete', async (req: BPRequest, res: Response) => {
const { userId, conversationId } = req.params

if (!validateUserId(userId)) {
return res.status(400).send(ERR_USER_ID_REQ)
}

bp.realtime.sendPayload(bp.RealTimePayload.forVisitor(userId, 'webchat.clear', { conversationId }))

await db.deleteConversationMessages(conversationId)

res.sendStatus(204)
})
}
13 changes: 11 additions & 2 deletions modules/channel-web/src/backend/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export default class WebchatDb {

constructor(private bp: typeof sdk) {
this.users = bp.users
this.knex = bp['database'] // TODO Fixme

this.knex = bp.database
this.batchSize = this.knex.isLite ? 40 : 2000

setInterval(() => this.flush(), ms('1s'))
Expand Down Expand Up @@ -367,6 +366,16 @@ export default class WebchatDb {
})
}

async deleteConversationMessages(conversationId: string) {
return this.knex.transaction(async trx => {
// TODO: Delete the related events using bp SDK

await trx('web_messages')
.del()
.where({ conversationId })
})
}

async getConversationMessages(conversationId, limit: number, fromId?: string): Promise<any> {
let query = this.knex('web_messages').where({ conversationId })

Expand Down
1 change: 1 addition & 0 deletions modules/channel-web/src/views/full/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class WebBotpressUIInjection extends React.Component {
useSessionStorage: false,
showPoweredBy: false,
enableResetSessionShortcut: true,
enableConversationDeletion: true,
containerWidth: EMULATOR_WIDTH,
layoutWidth: EMULATOR_WIDTH,
overrides: {
Expand Down
57 changes: 52 additions & 5 deletions modules/channel-web/src/views/lite/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { observe } from 'mobx'
import { inject, observer } from 'mobx-react'
import React from 'react'

import confirmDialog from '../../../../../../src/bp/ui-shared-lite/ConfirmDialog'
import MoreOptions from '../../../../../../src/bp/ui-shared-lite/MoreOptions'
import Close from '../icons/Close'
import Delete from '../icons/Delete'
import Download from '../icons/Download'
import Information from '../icons/Information'
import List from '../icons/List'
Expand Down Expand Up @@ -74,13 +76,46 @@ class Header extends React.Component<HeaderProps> {
)
}

handleDeleteConversation = async () => {
if (await confirmDialog(this.props.intl.formatMessage({
id: 'header.deleteConversation'
}), {
acceptLabel: this.props.intl.formatMessage({
id: 'header.deleteConversationYes'
}),
declineLabel: this.props.intl.formatMessage({
id: 'header.deleteConversationNo'
})
})
) {
await this.props.deleteConversation()
}
}

renderDeleteConversationButton() {
return (
<button
type="button"
tabIndex={-1}
id="btn-delete"
ref={el => (this.btnEls[0] = el)}
className={'bpw-header-icon bpw-header-icon-delete'}
onClick={this.handleDeleteConversation}
onKeyDown={this.handleKeyDown.bind(this, this.handleDeleteConversation)}
onBlur={this.onBlur}
>
<Delete />
</button>
)
}

renderResetButton() {
return (
<button
type="button"
tabIndex={-1}
id="btn-reset"
ref={el => (this.btnEls[0] = el)}
ref={el => (this.btnEls[1] = el)}
className={'bpw-header-icon bpw-header-icon-reset'}
onClick={this.props.resetSession}
onKeyDown={this.handleKeyDown.bind(this, this.props.resetSession)}
Expand All @@ -97,7 +132,7 @@ class Header extends React.Component<HeaderProps> {
type="button"
tabIndex={-1}
id="btn-download"
ref={el => (this.btnEls[1] = el)}
ref={el => (this.btnEls[2] = el)}
className={'bpw-header-icon bpw-header-icon-download'}
onClick={this.props.downloadConversation}
onKeyDown={this.handleKeyDown.bind(this, this.props.downloadConversation)}
Expand All @@ -114,7 +149,7 @@ class Header extends React.Component<HeaderProps> {
type="button"
tabIndex={-1}
id="btn-conversations"
ref={el => (this.btnEls[2] = el)}
ref={el => (this.btnEls[3] = el)}
className={'bpw-header-icon bpw-header-icon-convo'}
onClick={this.props.toggleConversations}
onKeyDown={this.handleKeyDown.bind(this, this.props.toggleConversations)}
Expand All @@ -131,7 +166,7 @@ class Header extends React.Component<HeaderProps> {
type="button"
tabIndex={-1}
id="btn-botinfo"
ref={el => (this.btnEls[3] = el)}
ref={el => (this.btnEls[4] = el)}
className={'bpw-header-icon bpw-header-icon-botinfo'}
onClick={this.props.toggleBotInfo}
onKeyDown={this.handleKeyDown.bind(this, this.props.toggleBotInfo)}
Expand All @@ -151,7 +186,7 @@ class Header extends React.Component<HeaderProps> {
id: 'header.hideChatWindow',
defaultMessage: 'Hide the chat window'
})}
ref={el => (this.btnEls[4] = el)}
ref={el => (this.btnEls[5] = el)}
className={'bpw-header-icon bpw-header-icon-close'}
onClick={this.props.hideChat}
onKeyDown={this.handleKeyDown.bind(this, this.props.hideChat)}
Expand Down Expand Up @@ -228,6 +263,13 @@ class Header extends React.Component<HeaderProps> {
})
}

if (this.props.showDeleteConversationButton) {
optionsItems.push({
label: 'Delete conversation',
action: this.renderDeleteConversationButton
})
}

if (this.props.isEmulator) {
return (
<div className="bpw-emulator-header">
Expand All @@ -249,6 +291,7 @@ class Header extends React.Component<HeaderProps> {
</div>
</div>
{!!this.props.customButtons.length && this.renderCustomButtons()}
{this.props.showDeleteConversationButton && this.renderDeleteConversationButton()}
{this.props.showResetButton && this.renderResetButton()}
{this.props.showDownloadButton && this.renderDownloadButton()}
{this.props.showConversationsButton && this.renderConvoButton()}
Expand All @@ -262,6 +305,7 @@ class Header extends React.Component<HeaderProps> {
export default inject(({ store }: { store: RootStore }) => ({
intl: store.intl,
isConversationsDisplayed: store.view.isConversationsDisplayed,
showDeleteConversationButton: store.view.showDeleteConversationButton,
showDownloadButton: store.view.showDownloadButton,
showBotInfoButton: store.view.showBotInfoButton,
showConversationsButton: store.view.showConversationsButton,
Expand All @@ -277,6 +321,7 @@ export default inject(({ store }: { store: RootStore }) => ({
toggleBotInfo: store.view.toggleBotInfo,
customButtons: store.view.customButtons,

deleteConversation: store.deleteConversation,
resetSession: store.resetSession,
downloadConversation: store.downloadConversation,
botName: store.botName,
Expand All @@ -300,13 +345,15 @@ type HeaderProps = Pick<
| 'hasUnreadMessages'
| 'unreadCount'
| 'hasBotInfoDescription'
| 'deleteConversation'
| 'resetSession'
| 'downloadConversation'
| 'toggleConversations'
| 'hideChat'
| 'toggleBotInfo'
| 'botAvatarUrl'
| 'showResetButton'
| 'showDeleteConversationButton'
| 'showDownloadButton'
| 'showConversationsButton'
| 'showBotInfoButton'
Expand Down
10 changes: 9 additions & 1 deletion modules/channel-web/src/views/lite/core/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class WebchatApi {
config => {
if (!config.url.includes('/botInfo')) {
const prefix = config.url.indexOf('?') > 0 ? '&' : '?'
config.url += prefix + '__ts=' + new Date().getTime()
config.url = `${config.url}${prefix}__ts=${new Date().getTime()}`
}
return config
},
Expand Down Expand Up @@ -135,6 +135,14 @@ export default class WebchatApi {
}
}

async deleteMessages(convoId: number) {
try {
await this.axios.post(`/conversations/${this.userId}/${convoId}/messages/delete`, {}, this.axiosConfig)
} catch (err) {
await this.handleApiError(err)
}
}

async sendFeedback(feedback: number, eventId: string): Promise<void> {
try {
return this.axios.post('/saveFeedback', { eventId, target: this.userId, feedback }, this.axiosConfig)
Expand Down
1 change: 1 addition & 0 deletions modules/channel-web/src/views/lite/core/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default {
showPoweredBy: window.SHOW_POWERED_BY,
enablePersistHistory: true,
enableResetSessionShortcut: false,
enableConversationDeletion: true,
closeOnEscape: true
}
}
2 changes: 2 additions & 0 deletions modules/channel-web/src/views/lite/core/socket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class BpSocket {
private userIdScope: string
private chatId: string | undefined

public onClear: (event: any) => void
public onMessage: (event: any) => void
public onTyping: (event: any) => void
public onData: (event: any) => void
Expand All @@ -25,6 +26,7 @@ export default class BpSocket {
// Connect the Botpress Web Socket to the server
this.events.setup(this.userIdScope)

this.events.on('guest.webchat.clear', this.onClear)
this.events.on('guest.webchat.message', this.onMessage)
this.events.on('guest.webchat.typing', this.onTyping)
this.events.on('guest.webchat.data', this.onData)
Expand Down
12 changes: 12 additions & 0 deletions modules/channel-web/src/views/lite/icons/Delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react'

export default () => (
<i>
<svg height="15" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fillRule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</g>
</svg>
</i>
)
1 change: 1 addition & 0 deletions modules/channel-web/src/views/lite/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@blueprintjs/core/lib/css/blueprint.css'
import { configure } from 'mobx'
import { inject, observer, Provider } from 'mobx-react'
import React from 'react'
Expand Down
19 changes: 16 additions & 3 deletions modules/channel-web/src/views/lite/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class Web extends React.Component<MainProps> {

async initializeSocket() {
this.socket = new BpSocket(this.props.bp, this.config)
this.socket.onClear = this.handleClearMessages
this.socket.onMessage = this.handleNewMessage
this.socket.onTyping = this.handleTyping
this.socket.onData = this.handleDataMessage
Expand Down Expand Up @@ -170,6 +171,10 @@ class Web extends React.Component<MainProps> {
})
}

isCurrentConversation = (event: Message) => {
return !this.props.config?.conversationId || Number(this.props.config.conversationId) === Number(event.conversationId)
}

handleIframeApi = async ({ data: { action, payload } }) => {
if (action === 'configure') {
this.props.updateConfig(Object.assign({}, constants.DEFAULT_CONFIG, payload))
Expand Down Expand Up @@ -200,13 +205,19 @@ class Web extends React.Component<MainProps> {
}
}

handleNewMessage = async event => {
handleClearMessages = (event: Message) => {
if (this.isCurrentConversation(event)) {
this.props.clearMessages()
}
}

handleNewMessage = async (event: Message) => {
if (event.payload?.type === 'visit' || event.message_type === 'visit') {
// don't do anything, it's the system message
return
}

if (this.props.config.conversationId && Number(this.props.config.conversationId) !== Number(event.conversationId)) {
if (!this.isCurrentConversation(event)) {
// don't do anything, it's a message from another conversation
return
}
Expand All @@ -224,7 +235,7 @@ class Web extends React.Component<MainProps> {
}

handleTyping = async (event: Message) => {
if (this.props.config.conversationId && Number(this.props.config.conversationId) !== Number(event.conversationId)) {
if (!this.isCurrentConversation(event)) {
// don't do anything, it's a message from another conversation
return
}
Expand Down Expand Up @@ -345,6 +356,7 @@ export default inject(({ store }: { store: RootStore }) => ({
updateConfig: store.updateConfig,
mergeConfig: store.mergeConfig,
addEventToConversation: store.addEventToConversation,
clearMessages: store.clearMessages,
setUserId: store.setUserId,
updateTyping: store.updateTyping,
sendMessage: store.sendMessage,
Expand Down Expand Up @@ -394,6 +406,7 @@ type MainProps = { store: RootStore } & Pick<
| 'hasUnreadMessages'
| 'showWidgetButton'
| 'addEventToConversation'
| 'clearMessages'
| 'updateConfig'
| 'mergeConfig'
| 'isWebchatReady'
Expand Down
14 changes: 14 additions & 0 deletions modules/channel-web/src/views/lite/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ class RootStore {
this.currentConversation.messages = messages
}

@action.bound
clearMessages() {
this.currentConversation.messages = []
}

@action.bound
async deleteConversation(): Promise<void> {
if (this.currentConversation !== undefined && this.currentConversation.messages.length > 0) {
await this.api.deleteMessages(this.currentConversationId)

this.clearMessages()
}
}

@action.bound
async addEventToConversation(event: Message): Promise<void> {
if (this.isInitialized && this.currentConversationId !== Number(event.conversationId)) {
Expand Down
7 changes: 7 additions & 0 deletions modules/channel-web/src/views/lite/store/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ class ViewStore {
return !this.isConversationsDisplayed && !this.isBotInfoDisplayed && this.rootStore.config.enableTranscriptDownload
}

@computed
get showDeleteConversationButton() {
return (
!this.isConversationsDisplayed && !this.isBotInfoDisplayed && this.rootStore.config.enableConversationDeletion
)
}

@computed
get showResetButton() {
return !this.isConversationsDisplayed && !this.isBotInfoDisplayed && this.rootStore.config?.enableReset
Expand Down
3 changes: 3 additions & 0 deletions modules/channel-web/src/views/lite/translations/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"composer.placeholderInit": "قل شيئا ل {name}",
"composer.send": "إرسال",
"header.conversations": "محادثات",
"header.deleteConversation": "هل أنت متأكد أنك تريد حذف هذه المحادثة بأكملها؟",
"header.deleteConversationYes": "حذف",
"header.deleteConversationNo": "إلغاء",
"conversationList.untitledConversation": "محادثة بلا عنوان",
"botInfo.backToConversation": "العودة الى المحادثة",
"botInfo.startConversation": "بداية محادثة",
Expand Down

0 comments on commit 742e28e

Please sign in to comment.