advanced-chat / vue-advanced-chat

A beautiful chat rooms web component compatible with all Javascript frameworks
https://advanced-chat.github.io/vue-advanced-chat/
MIT License
1.74k stars 469 forks source link
angular chat chat-application chatrooms firestore groups javascript react vue vue2 vue3 vuejs

vue-advanced-chat

Demo Image

Sponsors

ChatKitty sponsor
Easy to use chat API/Server with scalable infrastructure

Features

Demo

Enjoy :smile:

Real World Example

A Progressive Web Application showcasing all the features of vue-advanced-chat component.
Built with Firestore, Vuetify, and Push Notifications.

If you wish to get premium access to the real world example source code, please contact me by email.

You will get a fully working chat application for web and mobile:


Table of Contents


Installation

# Using npm
npm install --save vue-advanced-chat

# Using yarn
yarn add vue-advanced-chat

# Using CDN
<script src="https://cdn.jsdelivr.net/npm/vue-advanced-chat@2.0.4/dist/vue-advanced-chat.umd.js"></script>

Vue

Register vue-advanced-chat and emoji-picker as web components in your config file:

compilerOptions: {
  isCustomElement: tagName => {
    return tagName === 'vue-advanced-chat' || tagName === 'emoji-picker'
  }
}

Demo: https://github.com/advanced-chat/vue-advanced-chat-sandbox/tree/main

React

Demo: https://github.com/advanced-chat/vue-advanced-chat-sandbox/tree/react

Angular / Ionic

Demo: https://github.com/advanced-chat/vue-advanced-chat-sandbox/tree/angular


Usage

You can import it as a custom component:

<template>
  <vue-advanced-chat
    :current-user-id="currentUserId"
    :rooms="JSON.stringify(rooms)"
    :messages="JSON.stringify(messages)"
    :room-actions="JSON.stringify(roomActions)"
  />
</template>

<script>
  import { register } from 'vue-advanced-chat'
  register()

  // Or if you used CDN import
  // window['vue-advanced-chat'].register()

  export default {
    data() {
      return {
        currentUserId: '1234',
        rooms: [],
        messages: [],
        roomActions: [
          { name: 'inviteUser', title: 'Invite User' },
          { name: 'removeUser', title: 'Remove User' },
          { name: 'deleteRoom', title: 'Delete Room' }
        ]
      }
    }
  }
</script>

Important notes

vue-advanced-chat component is performance oriented, hence you have to follow specific rules to make it work properly.

// DO THIS
const rooms = []
for (let i = 0; i < res.length; i++) {
  rooms.push(res)
}
this.rooms = rooms

// DON'T DO THIS
for (let i = 0; i < res.length; i++) {
  this.rooms.push(res)
}
// DO THIS
this.rooms[i].typingUsers = [...this.rooms[i].typingUsers, typingUserId]

// DON'T DO THIS
this.rooms[i].typingUsers.push(typingUserId)
// DO THIS
this.rooms[roomIndex] = room
this.rooms = [...this.rooms]

// DON'T DO THIS
this.rooms[roomIndex] = room

// AND DON'T DO THIS
this.rooms.push(room)
fetchMessages({ room, options }) {
  this.messagesLoaded = false

  // use timeout to imitate async server fetched data
  setTimeout(() => {
    this.messages = []
    this.messagesLoaded = true
  })
}


Props API

Important notes

If you are using Vue 3, you can pass Array and Object props normally: Passing DOM Properties in Vue 3
Otherwise, you need to pass those props as strings. For example: [messages]="JSON.stringify(messages)"

Prop
Type Required Default
height String - 600px
current-user-id(1) String true -
rooms [String, Array] - []
rooms-order desc / asc - desc
loading-rooms(2) Boolean - false
rooms-loaded(3) Boolean - false
room-id(4) String - null
load-first-room(5) Boolean - true
rooms-list-opened Boolean - true
custom-search-room-enabled (6) Boolean - false
messages [String, Array] - []
room-message(7) String - null
username-options (8) [String, Object] - {minUsers: 3, currentUser: false}
messages-loaded(9) Boolean - false
room-actions(10) [String, Array] - []
menu-actions(11) [String, Array] - []
message-actions(12) [String, Array] - (11)
message-selection-actions(13) [String, Array] - (13)
templates-text(14) [String, Array] - null
auto-scroll(15) [String, Object] - { send: { new: true, newAfterScrollUp: true }, receive: { new: true, newAfterScrollUp: false } }
show-search Boolean - true
show-add-room Boolean - true
show-send-icon Boolean - true
show-files Boolean - true
show-audio Boolean - true
audio-bit-rate Number - 128
audio-sample-rate Number - 44100
show-emojis Boolean - true
show-reaction-emojis Boolean - true
show-new-messages-divider(16) Boolean - true
show-footer(17) Boolean - true
text-messages(18) [String, Object] - null
text-formatting(19) [String, Object] - {disabled: false, italic: '_', bold: '*', strike: '~', underline: '°', multilineCode: '```', inlineCode: ''}`
link-options(20) [String, Object] - { disabled: false, target: '_blank', rel: null }
room-info-enabled (21) Boolean - false
textarea-action-enabled(22) Boolean - false
textarea-auto-focus Boolean - true
user-tags-enabled Boolean - true
emojis-suggestion-enabled Boolean - true
media-preview-enabled Boolean - true
responsive-breakpoint(23) Number - 900
single-room(24) Boolean - false
scroll-distance(25) Number - 60
theme(26) light / dark - light
accepted-files(27) String - *
capture-files(28) String - ''
multiple-files(29) Boolean - true
styles(30) [String, Object] - (26)
show-audio(31) Boolean - true
emoji-data-source String - https://cdn.jsdelivr.net/npm/emoji-picker-element-data@%5E1/en/emojibase/data.json

(1) current-user-id is required to display UI and trigger actions according to the user using the chat (ex: messages position on the right, etc.)

(2) loading-rooms can be used to show/hide a spinner icon while rooms are loading

(3) rooms-loaded must be set to true when all rooms have been loaded. Meaning the user cannot scroll to load more paginated rooms

(4) room-id can be used to load a specific room at any time

(5) load-first-room can be used to remove the default behaviour of opening the first room at initialization

(6) custom-search-room-enabled can be used to use the search-room event to call your own method to filter out rooms

(7) room-message can be used to add a default textarea value

(8) username-options can be used to show/hide room messages username according to the minimum number of users minUsers inside a room, and if the message user is the current user currentUser

(9) messages-loaded must be set to true when all messages of a conversation have been loaded. Meaning the user cannot scroll on top to load more paginated messages

(10) room-actions can be used to display your own buttons when clicking the dropdown icon of each room inside the rooms list.
You can then use the room-action-handler event to call your own action after clicking a button. Ex:

room-actions="[
  {
    name: 'archiveRoom',
    title: 'Archive Room'
  }
]"

(11) menu-actions can be used to display your own buttons when clicking the vertical dots icon inside a room.
You can then use the menu-action-handler event to call your own action after clicking a button. Ex:

menu-actions="[
  {
    name: 'inviteUser',
    title: 'Invite User'
  },
  {
    name: 'removeUser',
    title: 'Remove User'
  },
  {
    name: 'deleteRoom',
    title: 'Delete Room'
  }
]"

(12) message-actions can be used to display your own buttons when clicking the dropdown icon inside a message.
You can then use the message-action-handler event to call your own action after clicking a button. Ex:

message-actions="[
  {
    name: 'addMessageToFavorite',
    title: 'Add To Favorite'
  },
  {
    name: 'shareMessage',
    title: 'Share Message'
  }
]"

You can use built-in message-actions names to trigger specific UI modifications when clicked.
Currently, replyMessage, editMessage and deleteMessage action names are available.
If messageActions is not set, it will use the default values below.
If you don't want to display this messageActions menu, you can pass it an empty array.

messageActions="[
  {
    name: 'replyMessage',
    title: 'Reply'
  },
  {
    name: 'editMessage',
    title: 'Edit Message',
    onlyMe: true
  },
  {
    name: 'deleteMessage',
    title: 'Delete Message',
    onlyMe: true
  },
  {
    name: 'selectMessages',
    title: 'Select'
  }
]"

(13) message-selection-actions is related to the above selectMessages message action. You can use it to display custom action buttons in the room header when selecting a message. Ex:

messageActions="[
  {
    name: 'deleteMessages',
    title: 'Delete'
  },
  {
    name: 'forwardMessages',
    title: 'Forward'
  }
]"

(14) templates-text can be used to add autocomplete templates text when typing / in the room textarea. Ex:

templatesText="[
  {
    tag: 'help',
    text: 'This is the help'
  },
  {
    tag: 'action',
    text: 'This is the action'
  }
]"

(15) auto-scroll can be used to change the auto scroll behaviour when sending/receiving a message. Ex:

auto-scroll="{
  send: {
    new: true, // will scroll down after sending a message
    newAfterScrollUp: false // will not scroll down after sending a message when scrolled up
  },
  receive: {
    new: false, // will not scroll down when receiving a message
    newAfterScrollUp: true // will scroll down when receiving a message when scrolled up
  }
}"

(16) show-new-messages-divider can be used to show/hide the blue line divider between seen and unseen messages.

(17) show-footer can be used to hide the room footer. For example to prevent users to send any message or media.

(18) text-messages can be used to replace default i18n texts. Ex:

text-messages="{
  ROOMS_EMPTY: 'Aucune conversation',
  ROOM_EMPTY: 'Aucune conversation sélectionnée',
  NEW_MESSAGES: 'Nouveaux messages',
  MESSAGE_DELETED: 'Ce message a été supprimé',
  MESSAGES_EMPTY: 'Aucun message',
  CONVERSATION_STARTED: 'La conversation a commencée le :',
  TYPE_MESSAGE: 'Tapez votre message',
  SEARCH: 'Rechercher',
  IS_ONLINE: 'est en ligne',
  LAST_SEEN: 'dernière connexion ',
  IS_TYPING: 'est en train de taper...',
  CANCEL_SELECT_MESSAGE: 'Annuler Sélection'
}"

(19) text-formatting can be used to add text formatting. Bold, italic, strikethrough, underline, inline code and multiline code formatting are currently available and can be used in conjonction.

Style Syntax Example Output
Bold * * *This is bold text* This is bold text
Italic _ _ _This text is italicized_ This text is italicized
Strikethrough ~ ~ ~This was mistaken text~ This was mistaken text
Underline ° ° °This text is underlined° This text is underlined
Nested formatting * * and _ _ *This text is _extremely_ important* This text is extremely important

Inline Code

Example: `This is inline code`

Output: This is inline code

Multiline Code

Example: ```This is multiline code```

Output:

This is
multiline code

(20) link-options can be used to disable url links in messages, or change urls target. Ex:

:link-options="{ disabled: true, target: '_self', rel: null }"

(21) room-info-enabled can be used to trigger an event after clicking the room header component.
You can then use the room-info event to call your own action after clicking the header.

(22) textarea-action-enabled can be used to add an extra icon on the right of the textarea
You can then use the textarea-action-handler event to call your own action after clicking the icon.

(23) responsive-breakpoint can be used to collapse the rooms list on the left when then viewport size goes below the specified width.

(24) single-room can be used if you never want to show the rooms list on the left. You still need to pass the rooms prop as an array with a single element.

(25) scroll-distance can be used to change the number of pixels before fetchMessages event is triggered when scrolling up to load more messages, or fetchMoreRooms event is triggered when scrolling down to load more rooms.

(26) theme can be used to change the chat theme. Currently, only light and dark are available.

(27) accepted-files can be used to set specifics file types allowed in chat. By default, all file types are allowed: "*".

Example: set accepted-files="image/png, image/jpeg, application/pdf" to allow JPG PNG and PDF files only

(28) capture-files can be used to enable direct capturing of photos and videos on mobile browsers, as opposed to just uploading existing photos and videos which are already on the device. See here for more information and recognized values. By default, the attribute is omitted and mobile browsers will only offer the gallery to choose photos and videos. Note: this only affects file attachments. Audio messages are always recorded using the device's microphone.

(29) multiple-files can be used to define whether multiple file selections will be accepted. By default this is true.

(30) styles can be used to customize your own theme. You can find the full list here

(31) show-audio can be used to enable or disable audio icon

styles="{
  general: {
    color: '#0a0a0a',
    colorSpinner: '#333',
    borderStyle: '1px solid #e1e4e8'
  },

  footer: {
    background: '#f8f9fa',
    backgroundReply: 'rgba(0, 0, 0, 0.08)'
  },

  icons: {
    search: '#9ca6af'
  }
}"

Props data structure

Your props must follow a specific structure to display rooms and messages correctly:

Rooms prop

rooms="[
  {
    roomId: '1',
    roomName: 'Room 1',
    avatar: 'assets/imgs/people.png',
    unreadCount: 4,
    index: 3,
    lastMessage: {
      _id: 'xyz',
      content: 'Last message received',
      senderId: '1234',
      username: 'John Doe',
      timestamp: '10:20',
      saved: true,
      distributed: false,
      seen: false,
      new: true
    },
    users: [
      {
        _id: '1234',
        username: 'John Doe',
        avatar: 'assets/imgs/doe.png',
        status: {
          state: 'online',
          lastChanged: 'today, 14:30'
        }
      },
      {
        _id: '4321',
        username: 'John Snow',
        avatar: 'assets/imgs/snow.png',
        status: {
          state: 'offline',
          lastChanged: '14 July, 20:00'
        }
      }
    ],
    typingUsers: [ 4321 ]
  }
]"

Messages prop

Message objects are rendered differently depending on their type. Text, emoji, image, video and file types are supported.

Each message object has a senderId field which holds the id of the corresponding agent. If senderId matches the currentUserId prop, specific UI and actions will be implemented.

Notes:

Message states:

messages="[
  {
    _id: '7890',
    indexId: 12092,
    content: 'Message 1',
    senderId: '1234',
    username: 'John Doe',
    avatar: 'assets/imgs/doe.png',
    date: '13 November',
    timestamp: '10:20',
    system: false,
    saved: true,
    distributed: true,
    seen: true,
    deleted: false,
    failure: true,
    disableActions: false,
    disableReactions: false,
    files: [
      {
        name: 'My File',
        size: 67351,
        type: 'png',
        audio: true,
        duration: 14.4,
        url: 'https://firebasestorage.googleapis.com/...',
        preview: 'data:image/png;base64,iVBORw0KGgoAA...',
        progress: 88
      }
    ],
    reactions: {
      😁: [
        '1234', // USER_ID
        '4321'
      ],
      🥰: [
        '1234'
      ]
    },
    replyMessage: {
      content: 'Reply Message',
      senderId: '4321',
      files: [
        {
          name: 'My Replied File',
          size: 67351,
          type: 'png',
          audio: true,
          duration: 14.4,
          url: 'https://firebasestorage.googleapis.com/...',
          preview: 'data:image/png;base64,iVBORw0KGgoAA...'
        }
      ]
    },
  }
]"


Events API

Event
Params Fires when a user
fetch-messages(1) { room, options } Scrolled on top to load more messages
fetch-more-rooms(2) - Scrolled to load more rooms
send-message { roomId, content, files(11), replyMessage(12), usersTag } Sent a message
edit-message { roomId, messageId, newContent, files(11), replyMessage(12) ,usersTag } Edited a message
delete-message { roomId, message } Deleted a message
open-file { message, file } Clicked to view or download a file
open-user-tag(3) { user } Clicked on a user tag inside a message
open-failed-message { roomId, message } Clicked on the failure icon next to a message
add-room - Clicked on the plus icon next to searchbar
search-room(4) { roomId, value } Typed a character in the room search input
room-action-handler(5) { roomId, action } Clicked on the vertical dots icon inside a room
menu-action-handler(6) { roomId, action } Clicked on the vertical dots icon inside a room
message-action-handler(7) { roomId, action, message } Clicked on the dropdown icon inside a message
message-selection-action-handler(8) { roomId, action, messages } Clicked on the select button inside a message
send-message-reaction { roomId, messageId, reaction, remove } Clicked on the emoji icon inside a message
room-info (9) room Clicked the room header bar
toggle-rooms-list { opened } Clicked on the toggle icon inside a room header
textarea-action-handler(10) { roomId, message } Clicked on custom icon inside the footer
typing-message { roomId, message } Started typing a message

(1) fetch-messages is triggered every time a room is opened. If the room is opened for the first time, the options param will hold reset: true.
(1) fetch-messages should be a method implementing a pagination system. Its purpose is to load older messages of a conversation when the user scroll on top.

(2) fetch-more-rooms is triggered when scrolling down the rooms list, and should be a method implementing a pagination system.

(3) open-user-tag is triggered when clicking a user tag inside a message. When creating a user tag by typing @ in the footer textarea and sending the message, the tag will be identified with the below pattern:

<usertag>TAGGED_USER_ID</usertag>

This will make the tag clickable inside a message. Ex: message tag content
send-message and edit-message events will handle that pattern for you and pass it in the content param.

(4) room-action-handler is the result of the room-actions prop.
When clicking a button from your room-actions array, room-action-handler will give you the name of the button that was click. Then you can do anything you want with it. Ex:

menuActionHandler({ roomId, action }) {
  switch (action.name) {
    case 'archiveRoom':
      // call a method to archive the room
  }
}

(5) menu-action-handler is the result of the menu-actions prop.
When clicking a button from your menu-actions array, menu-action-handler will give you the name of the button that was click. Then you can do anything you want with it. Ex:

menuActionHandler({ roomId, action }) {
  switch (action.name) {
    case 'inviteUser':
      // call a method to invite a user to the room
    case 'removeUser':
      // call a method to remove a user from the room
    case 'deleteRoom':
      // call a method to delete the room
  }
}

(6) search-room can be enabled by using custom-search-room-enabled prop. This will allow you to call your own method to filter out searched rooms.

(7) message-action-handler is the result of the message-actions prop.
When clicking a button from your message-actions array, message-action-handler will give you the name of the button that was click and the corresponding message data. Then you can do anything you want with it. Ex:

messageActionHandler({ roomId, action, message }) {
  switch (action.name) {
    case 'addMessageToFavorite':
      // call a method to add a message to the favorite list
    case 'shareMessage':
      // call a method to share the message with another user
  }
}

(8) message-selection-action-handler is the result of the message-selection-actions prop.
When clicking a button from your message-selection-actions array, message-selection-action-handler will give you the name of the button that was click and the corresponding selected messages data. Then you can do anything you want with it. Ex:

messageSelectionActionHandler({ roomId, action, message }) {
  switch (action.name) {
    case 'deleteMessages':
      // call a method to delete selected messaged
    case 'shareMessage':
      // call a method to share selected messaged with another user
  }
}

(9) room-info is the result of the room-info-enabled prop.

(10) textarea-action-handler is the result of the textarea-action-enabled prop.

(11) Array of files where each file contain: { blob, localUrl, name, size, type, extension }

(12) replyMessage object is available when the user replied to another message by clicking the corresponding icon, and contains the message information that was clicked.


Named Slots

Example:

<vue-advanced-chat>
  <div slot="room-header">
    This is a new room header
  </div>

  <div v-for="message in messages" :slot="'message_' + message._id">
    <div v-if="message.system">
      System message: {{ message.content }}
    </div>
    <div v-else>
      Normal message: {{ message.content }}
    </div>
  </div>

  <div v-for="message in messages" :slot="'message-avatar_' + message._id">
    New Avatar
  </div>
</vue-advanced-chat>
Slot
Action
custom-action-icon Add a custom icon inside the footer
rooms-header Add a template on top of rooms list (above the search bar)
room-list-item_{{ROOM_ID}} Replace the template of the room list items
room-list-info_{{ROOM_ID}} Replace the info of room list items
room-list-avatar_{{ROOM_ID}} Replace the avatar of room list items
room-list-options_{{ROOM_ID}} Replace the template of the list room options
room-list-options-icon_{{ROOM_ID}} Replace the room list options dropdown icon
rooms-header Replace the content above the search bar
rooms-list-search Replace the search bar
room-header Replace the template of the room header
room-header-avatar Replace the template of the room header avatar
room-header-info Replace the template of the room header text
room-options Replace the template of the room options
message_{{MESSAGE_ID}} Replace the template of the message (and system message) box
message-avatar_{{MESSAGE_ID}} Replace the template of the message avatar
message-failure_{{MESSAGE_ID}} Replace the message failure icon
messages-empty Replace the empty message template
rooms-empty Replace the empty rooms template
no-room-selected Replace the no room selected template
menu-icon Replace the room menu icon
toggle-icon Replace the toggle room list icon
spinner-icon-rooms Replace the loading spinner icon in the rooms list
spinner-icon-infinite-rooms Replace the loading spinner icon to load more rooms
spinner-icon-messages Replace the loading spinner icon in a room
spinner-icon-infinite-messages Replace the loading spinner icon to load more messages
spinner-icon-room-file Replace the loading spinner icon to load heavy files
spinner-icon-message-file_{{MESSAGE_ID}} Replace the loading spinner icon in a message containing an image
scroll-icon Replace the scroll to newest message icon
reply-close-icon Replace the reply close icon
image-close-icon Replace the image close icon
file-icon Replace the file icon
files-close-icon Replace the file close icon
edit-close-icon Replace the edit close icon
preview-close-icon Replace the media preview close icon
emoji-picker-icon Replace the emoji picker icon
emoji-picker-reaction-icon_{{MESSAGE_ID}} Replace the emoji picker reaction icon (in the message box)
paperclip-icon Replace the paperclip icon
send-icon Replace the message send icon
eye-icon_{{MESSAGE_ID}} Replace the eye icon (image message)
document-icon_{{MESSAGE_ID}} Replace the document icon
pencil-icon_{{MESSAGE_ID}} Replace the pencil icon
checkmark-icon_{{MESSAGE_ID}} Replace the checkmark icon of a message
checkmark-icon_{{ROOM_ID}} Replace the checkmark icon of a message from the rooms list
deleted-icon_{{MESSAGE_ID}} Replace the deleted icon of a message
deleted-icon_{{ROOM_ID}} Replace the deleted icon of a message from the rooms list
microphone-icon_{{ROOM_ID}} Replace the microphone icon
dropdown-icon_{{MESSAGE_ID}} Replace the dropdown icon
search-icon Replace the search icon
add-icon Replace the add room icon
audio-stop-icon Replace the audio recorder stop icon
audio-check-icon Replace the audio recorder confirm icon
audio-pause-icon_{{MESSAGE_ID}} Replace the message audio pause icon
audio-play-icon_{{MESSAGE_ID}} Replace the message audio play icon


Using with Firestore

Source code

You can find the source code to implement a full featured chat app using Firebase/Firestore inside the demo folder.
To test it using your own Firebase project:

Data structure

If you decide to use the same code as in the demo folder to create your chat app, you need to have a specific Firestore data structure.
To help you get started, I added in demo/src/App.vue a method addData to initialize some data on your Firestore database.

Users collection

users: {
  USER_ID_1: {
    _id: '1',
    username: 'User 1'
  },
  USER_ID_2: {
    _id: '2',
    username: 'User 2'
  },
  USER_ID_3: {
    _id: '3',
    username: 'User 2'
  }
}

Rooms collection

chatRooms: {
  ROOM_ID_1: {
    users: ['1', '3']
  },
  ROOM_ID_2: {
    users: ['1', '2', '3']
  }
}

Messages collection inside a room document

messages: {
  MESSAGE_ID_1: {
    content: 'My first message to <usertag>John</usertag>',
    senderId: '2',
    timestamp: 'December 11, 2019 at 4:00:00 PM',
    seen: true
  }
}

Notes


Contributing

Your help is always appreciated :rocket:

License

This project is licensed under MIT License