diff --git a/.gitignore b/.gitignore index 7fbb6e5c..5e5788ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,516 @@ -# Created by .ignore support plugin (hsz.mobi) -web-widget/node_modules -package-lock.json + +# Created by https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,react,reactnative +# Edit at https://www.toptal.com/developers/gitignore?templates=node,jetbrains+all,visualstudiocode,react,reactnative + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache +.stylelintcache + +# SvelteKit build / generate output +.svelte-kit + +### react ### +.DS_* +**/*.backup.* +**/*.back.* + node_modules -.idea -web-basic-sample/dist -web-basic-sample-syncmanager/dist -web-live-chat/dist -npm-debug.log -web-widget/dist + +*.sublime* + +psd +thumb +sketch + +### ReactNative ### +# React Native Stack Base + +.expo +__generated__ + +### ReactNative.Android Stack ### +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/jarRepositories.xml +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +### ReactNative.Buck Stack ### +buck-out/ +.buckconfig.local +.buckd/ +.buckversion +.fakebuckversion + +### ReactNative.Gradle Stack ### +.gradle + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### ReactNative.Linux Stack ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### ReactNative.Node Stack ### +# Logs + +# Diagnostic reports (https://nodejs.org/api/report.html) + +# Runtime data + +# Directory for instrumented libs generated by jscoverage/JSCover + +# Coverage directory used by tools like istanbul + +# nyc test coverage + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +# Bower dependency directory (https://bower.io/) + +# node-waf configuration + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +# Dependency directories + +# Snowpack dependency directory (https://snowpack.dev/) + +# TypeScript cache + +# Optional npm cache directory + +# Optional eslint cache + +# Microbundle cache + +# Optional REPL history + +# Output of 'npm pack' + +# Yarn Integrity file + +# dotenv environment variables file + +# parcel-bundler cache (https://parceljs.org/) + +# Next.js build output + +# Nuxt.js build / generate output + +# Gatsby files +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output + +# Serverless directories + +# FuseBox cache + +# DynamoDB Local files + +# TernJS port file + +# Stores VSCode versions used for testing VSCode extensions + +# yarn v2 + +### ReactNative.Xcode Stack ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Gcc Patch +/*.gcno + +### ReactNative.macOS Stack ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +!.vscode/*.code-snippets + +# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,react,reactnative + package-lock.json -Pods/ -Podfile.lock -.env \ No newline at end of file +yarn.lock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index a9b22514..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,6 +0,0 @@ -# Contributing - -Our code maintenance policy doesn't allow to merge PR from external source. If you find a bug in this repository and want to report it, please report it in the ISSUE tab or send an email to 'support@sendbird.com'. - -Thank you for your interest in SendBird JavaScript SDK. - \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 247aff3f..a8363dda 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 SendBird +Copyright (c) 2019 Sendbird Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 97b2c1bf..623dc870 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,75 @@ -# SendBird JavaScript Sample +# Sendbird JavaScript SDK v3 samples +![Platform](https://img.shields.io/badge/platform-JAVASCRIPT-orange.svg) +![Languages](https://img.shields.io/badge/language-JAVASCRIPT-orange.svg) +[![npm](https://img.shields.io/npm/v/sendbird.svg?style=popout&colorB=red)](https://www.npmjs.com/package/sendbird) -The samples in this repository are fully functional messaging applications built using [SendBird](https://sendbird.com) JavaScript SDK. - 1. **Web Basic sample:** Slack-like full screen chat sample for desktop browsers. - 2. **Web Basic sample with SyncManager** Same as Web Basic sample with SyncManager integrated - 3. **Web Widget sample:** Facebook-chat-like chat widget to regular websites. - 4. **Web Live Chat sample:** Twitch-chat-like chat sample for desktop browsers. - 5. **React Native Redux sample:** Mobile chat sample for iOS and Android. +## Deprecation Note (v3) +:warning: Please note that Sendbird’s SDK v3 will be deprecated by **July 2023**. You may still use the older SDKs at your choice, but no new updates or bug fixes will be made to SDK v3. -> `react-native-sample` and `web-sample` are deprecated. See `react-native-redux-sample` and `web-basic-sample` instead. +**We recommend clients to plan their migration to SDK v4 as early as possible as there are breaking changes.** We also provide prioritized support for migration and any issues related to v4. SDK v4 provides far richer and robust features in Websocket, Local caching, Polls, Scheduled Messages, Pinned Message, and many more. So try it out now! ([Chat SDK v4 react samples](https://github.com/sendbird/sendbird-chat-sample-react/)) +
-## Table of Contents +## 🔒 Security tip +When a new Sendbird application is created in the dashboard the default security settings are set permissive to simplify running samples and implementing your first code. - 1. [Installing SendBird JS SDK](#installing-the-sendbird-js-sdk) - 2. [Previous versions](#previous-versions) - - -## Installing the SendBird JS SDK - -Using [Bower](http://bower.io): +Before launching make sure to review the security tab under ⚙️ Settings -> Security, and set Access token permission to Read Only or Disabled so that unauthenticated users can not login as someone else. And review the Access Control lists. Most apps will want to disable "Allow retrieving user list" as that could expose usage numbers and other information. - bower install sendbird +## Introduction +This repository contains samples for how to use Sendbird to add chat using Javascript, React and React Native. You can find more information in the [Javascript SDK documentation](https://sendbird.com/docs/chat/v3/javascript/getting-started/about-chat-sdk) and [React Quickstart Documentation](https://sendbird.com/docs/uikit/v1/react/quickstart/send-first-message) -Using [npm](https://www.npmjs.com/package/sendbird): +![UIKit](asset/uikit.png) - npm install --save sendbird +### React +Sendbird UIKit for React is a set of prebuilt UI components that allows you to easily craft an in-app chat with all the essential messaging features. Our development kit includes light and dark themes, text fonts, colors and more. All the included components can be styled and customized to create an unique experience that fits your app. -> We support React Native and NodeJS. +- [**Basic React App**](https://github.com/sendbird/SendBird-JavaScript/tree/master/react/react-app-simple) is a quickest way to get started using UIKit +- [**Composed React App**](https://github.com/sendbird/SendBird-JavaScript/tree/master/react/react-app-custom) demonstrates how to use the various smart components. -### Manual download - -Or, you can manually download JS SDK files [here](https://github.com/sendbird/SendBird-SDK-JavaScript). +- [**Custom React App**](https://github.com/sendbird/SendBird-JavaScript/tree/master/react/react-app-custom) shows how to customize the **Message**, **ChannelPreview**, and **UserList** UI elements. -## Previous versions +### React Native +The Sendbird React Native framework allows you to simplify development for iOS and Android apps, and reuse the same code on both web and mobile apps. -To view the version 2 sample, checkout the `v2` branch instead of `master`. -To view the basic sample used `jQuery`, checkout the [Legacy tag](https://github.com/sendbird/SendBird-JavaScript/tree/Legacy(WebBasic)) instead of `master`. \ No newline at end of file +- [**React native Redux**](https://github.com/sendbird/SendBird-JavaScript/tree/master/react-native/react-native-redux) shows how to use Sendbird with React Native on iOS and Android. + +- [**React native Redux Syncmanager**](https://github.com/sendbird/SendBird-JavaScript/tree/master/react-native/react-native-redux-syncmanager) Expands on the above sample and implements the [Sendbird SyncManager](https://github.com/sendbird/sendbird-syncmanager-javascript) + +- [**React Native Hooks**](https://github.com/sendbird/SendBird-JavaScript/tree/master/react-native/react-native-hook) Implements Sendbird on iOS and Android using the hooks pattern. + + +### JavaScript + +- [**JavaScript chat sample**](https://github.com/sendbird/SendBird-JavaScript/tree/master/javascript/javascript-basic) is a Slack-like full screen chat sample for desktop browsers using both Group channels and open channels. + +- [**JavaScript widget sample**](https://github.com/sendbird/SendBird-JavaScript/tree/master/javascript/javascript-widget) is a Facebook-chat-like chat widget for websites. + +- [**JavaScript live chat sample**](https://github.com/sendbird/SendBird-JavaScript/tree/master/javascript/javascript-live-chat) is a Twitch-chat-like experience. + +- [**JavaScript chat sample with SyncManager**](https://github.com/sendbird/sendbird-javascript-samples/tree/master/javascript/javascript-basic-syncmanager) is a web chat sample integrated with [Sendbird SyncManager document](https://sendbird.com/docs/syncmanager/v1/javascript/getting-started/about-syncmanager), adds local caching to the core chat features. For faster data loading and caching, the sample synchronizes with the Sendbird server and saves a list of group channels and the messages within the local cache into your client app. + +## Installation + +To use the Sendbird Chat SDK directly you can install it through npm or yarn with + +```bash +npm install --save sendbird +``` +or + +```bash +yarn install --save sendbird +``` + +Or download the latest release manually from [GitHub](https://github.com/sendbird/SendBird-SDK-JavaScript) + + +## Getting Help + +Check out the [UIKit for React docs](https://sendbird.com/docs/uikit/v1/javascript/getting-started/about-uikit). and Sendbird's [Developer Portal](https://sendbird.com/developer) for tutorials and videos. If you need any help in resolving any issues or have questions, visit our [community forums](https://community.sendbird.com/c/sendbird-chat/12). + +## We are Hiring! +Sendbird is made up of a diverse group of humble, friendly, and hardworking individuals united by a shared purpose to build the next generation of mobile & social technologies. Join our team remotely or at one of our locations in San Mateo, Seoul, New York, London, and Singapore. More information on a [careers page](https://sendbird.com/careers). diff --git a/asset/reactnative-sample-thumbnail.jpg b/asset/reactnative-sample-thumbnail.jpg new file mode 100644 index 00000000..5e9ac8ec Binary files /dev/null and b/asset/reactnative-sample-thumbnail.jpg differ diff --git a/asset/uikit.png b/asset/uikit.png new file mode 100644 index 00000000..6f13c4c6 Binary files /dev/null and b/asset/uikit.png differ diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 00000000..63286af8 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,12 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: sendbird-javascript-samples + description: A guide of the installation and functions of Sendbird Chat, UIKit, and SyncManager for JavaScript samples + annotations: + github.com/project-slug: sendbird/sendbird-javascript-samples +spec: + type: library + lifecycle: production + owner: dep-client-platform + system: sendbird-chat diff --git a/index.css b/index.css deleted file mode 100644 index f3aa1c25..00000000 --- a/index.css +++ /dev/null @@ -1,126 +0,0 @@ -body { - font-family: 'Exo 2'; - -webkit-font-smoothing: antialiased; - background-color: #ffffff; -} - -a, a:link, a:visited, a:hover, a:active { - text-decoration: none !important; - color: initial; -} - -.container { - width: 900px; - height: 100%; - margin: 0 auto; - padding: 50px 0; -} - -.top { - width: 900px; - text-align: center; - font-size: 36px; - font-weight: 600; -} - -.hr { - width: 100%; - height: 0; - margin: 26px 0; - border-top: 1px solid #8e8e8e; -} - -.content { - width: 900px; - height: 100%; - margin-bottom: 20px; -} -.item { - display: inline-block; - width: 100%; - box-sizing: border-box; - padding: 20px; - border: 1px solid #2C2D30; - border-radius: 4px; - cursor: pointer; -} -.item:hover { - border: 1px solid #6742d6; -} - -.item-image { - width: 240px; - height: 150px; - float: left; - border-radius: 4px; - background-size: cover; - background-position: center center; - background-repeat: no-repeat; -} -.basic { - background-image: url('https://dxstmhyqfqr1o.cloudfront.net/screenshots/screenshot-1.png'); -} -.uikit { - background-image: url('https://dxstmhyqfqr1o.cloudfront.net/screenshots/screenshot-4.png'); -} -.widget { - background-image: url('https://dxstmhyqfqr1o.cloudfront.net/js-sample/widget-sample-1.png'); -} -.live-chat { - background-image: url('https://dxstmhyqfqr1o.cloudfront.net/js-sample/live-chat-sample.png'); -} -.rn-android { - background-image: url('https://dxstmhyqfqr1o.cloudfront.net/screenshots/screenshot-3.png'); - background-size: 75px 150px; - background-color: #f5f5f5; -} - -.item-info { - position: relative; - width: calc(100% - 280px); - height: 150px; - float: right; -} -.info-title { - font-size: 24px; - margin-bottom: 10px; -} -.info-desc { - font-size: 16px; - opacity: 0.5; -} -.info-button { - position: absolute; - bottom: 0; - right: 0; - text-align: right; - display: inline-block; - width: 100%; -} -.button { - float: right; - width: 100px; - font-size: 16px; - height: 30px; - text-align: center; - line-height: 30px; - border-radius: 4px; - margin-left: 10px; - cursor: pointer; - border: 1px solid #2C2D30; -} -.button > i { - margin-right: 8px; -} -.button:hover { - border: 1px solid #6742d6; - color: #6742d6; -} -.disable { - cursor: default; - opacity: 0.4; -} -.button.disable:hover { - border: 1px solid #2C2D30; - color: initial; -} \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index d2bca335..00000000 --- a/index.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - - - - - - - Sample | SendBird - - -
-
SendBird JavaScript Sample
-
- - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - -
- - \ No newline at end of file diff --git a/web-basic-sample-syncmanager/.babelrc b/javascript/javascript-basic-local-caching/.babelrc similarity index 100% rename from web-basic-sample-syncmanager/.babelrc rename to javascript/javascript-basic-local-caching/.babelrc diff --git a/web-basic-sample-syncmanager/.eslintignore b/javascript/javascript-basic-local-caching/.eslintignore similarity index 100% rename from web-basic-sample-syncmanager/.eslintignore rename to javascript/javascript-basic-local-caching/.eslintignore diff --git a/web-basic-sample-syncmanager/.eslintrc.js b/javascript/javascript-basic-local-caching/.eslintrc.js similarity index 100% rename from web-basic-sample-syncmanager/.eslintrc.js rename to javascript/javascript-basic-local-caching/.eslintrc.js diff --git a/web-basic-sample-syncmanager/.prettierignore b/javascript/javascript-basic-local-caching/.prettierignore similarity index 100% rename from web-basic-sample-syncmanager/.prettierignore rename to javascript/javascript-basic-local-caching/.prettierignore diff --git a/javascript/javascript-basic-local-caching/.prettierrc b/javascript/javascript-basic-local-caching/.prettierrc new file mode 100644 index 00000000..e90d7232 --- /dev/null +++ b/javascript/javascript-basic-local-caching/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "printWidth": 120, + "trailingComma": "none", + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/javascript/javascript-basic-local-caching/README.md b/javascript/javascript-basic-local-caching/README.md new file mode 100644 index 00000000..e06d3c33 --- /dev/null +++ b/javascript/javascript-basic-local-caching/README.md @@ -0,0 +1,81 @@ +# Sendbird Local Caching for JavaScript sample +![Platform](https://img.shields.io/badge/platform-JAVASCRIPT-orange.svg) +![Languages](https://img.shields.io/badge/language-JAVASCRIPT-orange.svg) +[![npm](https://img.shields.io/npm/v/sendbird.svg?style=popout&colorB=red)](https://www.npmjs.com/package/sendbird) + +## Introduction + +Local Caching enables Sendbird Chat SDK for JavaScript to locally cache and retrieve group channel and message data. This facilitates offline messaging by allowing the SDK to create a channel list view or a chat view in a prompt manner and display them even when a client app is in offline mode. Provided here is a Local Caching for JavaScript sample to experience first-hand the benefits of Sendbird's Local Caching. + +### Benefits + +Sendbird Local Caching provides the local caching system and data synchronization with the Sendbird server, which are run on an event-driven structure. According to the real-time events of the messages and channels, Local Caching takes care of the background tasks for the cache updates from the Sendbird server to the local device. By leveraging this systemized structure with connection-based synchronization, Local Caching allows you to easily integrate the Chat SDK to utilize all of its features, while also reducing data usage and offering a reliable and effortless storage mechanism. + +### More about Sendbird Local Caching for JavaScript + +Find out more about Sendbird Local Caching for JavaScript at [Local Caching for JavaScript doc](https://sendbird.com/docs/chat/v3/javascript/guides/local-caching). If you need any help in resolving any issues or have questions, visit [our community](https://community.sendbird.com). + +
+ +## Before getting started +This section provides the prerequisites for testing Sendbird Desk for JavaScript sample app. + +### Requirements +The minimum requirements for Local Caching for JavaScript are: +- Node.js v10+ +- NPM v6+ +- [Chat SDK for JavaScript](https://github.com/sendbird/SendBird-SDK-JavaScript) v3.1.0 or higher + +### Try the sample app using your data + +If you would like to try the sample app specifically fit to your usage, you can do so by replacing the default sample app ID with yours, which you can obtain by [creating your Sendbird application from the dashboard](https://sendbird.com/docs/chat/v3/javascript/getting-started/install-chat-sdk#2-step-1-create-a-sendbird-application-from-your-dashboard). Furthermore, you could also add data of your choice on the dashboard to test. This will allow you to experience the sample app with data from your Sendbird application. + +
+ +## Getting started + +You can install and run Local Caching for JavaScript sample app on your system using `npm`. + +### Install packages + +`Node` v8.x+ should be installed on your system. + +```bash +npm install +``` + +### Run the sample + +```bash +npm start +``` + +
+ +## Customizing the sample + +To implement customization to the sample, you can use `webpack` for buiding it. + +### Install packages + +`Node` v8.x+ should be installed on your system. + +```bash +npm install +``` + +### Modify files + +If you want to change `APP_ID`, change `APP_ID` in `const.js` to the other `APP_ID` you want. You can test the sample with local server by running the following command. + +```bash +npm run start:dev +``` + +### Build the sample + +When the modification is complete, you'll need to bundle the file using `webpack`. The bundled files are created in the **dist** folder. Please check `webpack.config.js` for settings. + +```bash +npm run build +``` diff --git a/javascript/javascript-basic-local-caching/chat.html b/javascript/javascript-basic-local-caching/chat.html new file mode 100644 index 00000000..00977a48 --- /dev/null +++ b/javascript/javascript-basic-local-caching/chat.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + Basic Sample with Local Caching | Sendbird + + + +
+
+
+ +
+ Sendbird +
+
+
+
+
GROUP CHAT
+
+
+
+
+
+
Start by inviting user to create a channel.
+
+
+
+
+ +
+
+
username
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/javascript/javascript-basic-local-caching/index.html b/javascript/javascript-basic-local-caching/index.html new file mode 100644 index 00000000..b9c984b8 --- /dev/null +++ b/javascript/javascript-basic-local-caching/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + Basic Sample with Local Caching | Sendbird + + + +
+
+ +
+
+ Sendbird +
+
+ Web Basic Sample with Local Caching +
+
+
+ +
+
+ Start chatting on Sendbird by choosing your display name. +
This can be changed anytime and will be shown on 1-on-1 and group messaging. + +
+
+ + + +
+
+ +
+ +
+
+ + + + diff --git a/javascript/javascript-basic-local-caching/migration.ts b/javascript/javascript-basic-local-caching/migration.ts new file mode 100644 index 00000000..adf743fc --- /dev/null +++ b/javascript/javascript-basic-local-caching/migration.ts @@ -0,0 +1,382 @@ +const sendbird = new SendBird({ appId: APP_ID }); + +const options = new SendBirdSyncManager.Options(); +options.messageCollectionCapacity = MESSAGE_COLLECTION_CAPACITY; +options.messageResendPolicy = MESSAGE_RESEND_POLICY; +options.automaticMessageResendRetryCount = AUTOMATIC_MESSAGE_RESEND_RETRY_COUNT; +options.maxFailedMessageCountPerChannel = MAX_FAILED_MESSAGE_COUNT_PER_CHANNEL; +options.failedMessageRetentionDays = FAILED_MESSAGE_RETENTION_DAYS; + +SendBirdSyncManager.sendbird = sendbird; +SendBirdSyncManager.setup(USER_ID, options, () => { + sendbird.setErrorFirstCallback(true); + sendbird + .connect(USER_ID) + .then(user => { + // Do something... + }) + .catch(error => { + // Handle error. + }); +}); + +const sendbird = new SendBird({ appId: APP_ID, localCacheEnabled: true }); +sendbird.setErrorFirstCallback(true); +sendbird + .connect(USER_ID) + .then(user => { + // Do something... + }) + .catch(error => { + // Handle error. + }); + +const myGroupChannelListQuery = sendbird.GroupChannel.createMyGroupChannelListQuery(); +myGroupChannelListQuery.includeEmpty = INCLUDE_EMPTY; +myGroupChannelListQuery.order = ORDER; +myGroupChannelListQuery.limit = LIMIT; + +const channelCollection = new SendBirdSyncManager.ChannelCollection(myGroupChannelListQuery); + +const groupChannelFilter = new sendbird.GroupChannelFilter(); +groupChannelFilter.includeEmpty = INCLUDE_EMPTY; + +const groupChannelCollection = sendbird.GroupChannel.createGroupChannelCollection() + .setOrder(ORDER) + .setFilter(FILTER) + .setLimit(LIMIT) + .build(); + +const collectionHandler = new SendBirdSyncManager.ChannelCollection.CollectionHandler(); +collectionHandler.onChannelEvent = (action, channels) => { + switch (action) { + case 'insert': + // Do something... + break; + case 'update': + // Do something... + break; + case 'move': + // Do something... + break; + case 'remove': + // Do something... + break; + case 'clear': + // Do something... + break; + } +}; +channelCollection.setCollectionHandler(collectionHandler); + +channelCollection.setGroupChannelCollectionHandler({ + onChannelsAdded: (context, channels) => { + // Do something... + }, + onChannelsUpdated: (context, channels) => { + // Do something... + }, + onChannelsDeleted: (context, channelUrls) => { + // Do something... + } +}); + +channelCollection.fetch(() => { + // Do something... +}); + +if (groupChannelCollection.hasMore) { + groupChannelCollection + .loadMore() + .then(channels => { + // Do something... + }) + .catch(error => { + // Handle error. + }); +} + +channelCollection.setCollectionHandler(null); +channelCollection.remove(); + +groupChannelCollection.dispose(); + +const messageFilter = { + messageTypeFilter: MESSAGE_TYPE_FILTER +}; +const messageCollection = new SendBirdSyncManager.MessageCollection(channel, messageFilter, STARTING_POINT); +messageCollection.limit = LIMIT; + +const messageFilter = new sendbird.MessageFilter(); +messageFilter.messageType = MESSAGE_TYPE; +const messageCollection = channel + .createMessageCollection() + .setFilter(messageFilter) + .setStartingPoint(STARTING_POINT) + .setLimit(LIMIT) + .build(); + +const messageCollectionHandler = new SendBirdSyncManager.MessageCollection.CollectionHandler(); +messageCollectionHandler.onPendingMessageEvent = (messages, action) => { + // Do something... +}; +messageCollectionHandler.onSucceededMessageEvent = (messages, action) => { + // Do something... +}; +messageCollectionHandler.onFailedMessageEvent = (messages, action, reason) => { + // Do something... +}; +messageCollectionHandler.onNewMessage = message => { + // Do something... +}; +messageCollectionHandler.onMessageEvent = (action, messages, action) => { + // Do something... +}; +messageCollectionHandler.onChannelUpdated = channel => { + // Do something... +}; +messageCollectionHandler.onChannelDeleted = channel => { + // Do something... +}; +messageCollection.setCollectionHandler(messageCollectionHandler); + +messageCollection.setMessageCollectionHandler({ + onMessagesAdded: (context, channel, messages) => { + // Do something... + }, + onMessagesUpdated: (context, channel, messages) => { + // Do something... + }, + onMessagesDeleted: (context, channel, messages) => { + // Do something... + }, + onChannelUpdated: (context, channel) => { + // Do something... + }, + onChannelDeleted: (context, channel) => { + // Do something... + }, + onHugeGapDetected: () => { + // Do something... + } +}); + +messageCollection + .initialize(MESSAGE_COLLECTION_INIT_POLICY) + .onCacheResult((error, messages) => { + if (error) { + // Handle error. + } + // Do something... + }) + .onApiResult((error, messages) => { + if (error) { + // Handle error. + } + // Do something... + }); + +messageCollection.fetchSucceededMessages('next', error => { + if (error) { + // Handle error; + } + // Do something... +}); + +if (messageCollection.hasNext) { + messageCollection + .loadNext() + .then(messages => { + // Do something... + }) + .catch(error => { + // Handle error. + }); +} + +messageCollection.fetchSucceededMessages('prev', error => { + if (error) { + // Handle error; + } + // Do something... +}); + +if (messageCollection.hasPrevious) { + messageCollection + .loadPrevious() + .then(messages => { + // Do something... + }) + .catch(error => { + // Handle error. + }); +} + +const pendingMessage = channel.sendUserMessage(USER_MESSAGE_PARAMS, (error, message) => { + messageCollection.handleSendMessageResponse(error, message); +}); +messageCollection.appendMessage(pendingMessage); + +channel.sendUserMessage(USER_MESSAGE_PARAMS, (error, message) => { + /** + * Pending message will be delivered to `MessageCollectionHandler.onMessagesAdded()`. + * The result of sending, either a succeeded or failed message, will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + * Do NOT add the pending, succeeded or failed message objects from the return value of `sendUserMessage()`, `sendFileMessage()`, and from the callback. + */ +}); + +const pendingMessage = channel.sendFileMessage(FILE_MESSAGE_PARAMS, (error, message) => { + messageCollection.handleSendMessageResponse(error, message); +}); +messageCollection.appendMessage(pendingMessage); + +channel.sendFileMessage(FILE_MESSAGE_PARAMS, (error, message) => { + /** + * Pending message will be delivered to `MessageCollectionHandler.onMessagesAdded()`. + * The result of sending, either a succeeded or failed message, will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + * Do NOT add the pending, succeeded or failed message objects from the return value of `sendUserMessage()`, `sendFileMessage()`, and from the callback. + */ +}); + +const pendingMessage = channel.resendUserMessage(failedMessage, (error, message) => { + messageCollection.handleSendMessageResponse(error, message); +}); +messageCollection.appendMessage(pendingMessage); + +channel.resendUserMessage(failedMessage, (error, message) => { + /** + * Pending message will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + * The result of sending, either a succeeded or failed message, will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + * Do NOT add the pending, succeeded or failed message objects from the return value of `resendUserMessage()`, `resendFileMessage()`, and from the callback. + */ +}); + +const pendingMessage = channel.resendFileMessage(failedMessage, (error, message) => { + messageCollection.handleSendMessageResponse(error, message); +}); +messageCollection.appendMessage(pendingMessage); + +channel.resendFileMessage(failedMessage, (error, message) => { + /** + * Pending message will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + * The result of sending, either a succeeded or failed message, will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + * Do NOT add the pending, succeeded or failed message objects from the return value of `resendUserMessage()`, `resendFileMessage()`, and from the callback. + */ +}); + +messageCollection.setCollectionHandler(null); +messageCollection.remove(); + +messageCollection.dispose(); + +messageCollectionHandler.onNewMessage = message => { + // Do something... +}; + +const channelHandler = new sendbird.ChannelHandler(); +channelHandler.onMessageReceived = (channel, message) => { + // Do something... +}; +sendbird.addChannelHandler(CHANNEL_HANDLER_KEY, channelHandler); + +channel.updateUserMessage(message.messageId, USER_MESSAGE_PARAMS, (error, message) => { + messageCollection.updateMessage(message); +}); + +channel.updateUserMessage(message.messageId, USER_MESSAGE_PARAMS, (error, message) => { + /** + * Updated message will be handled internally and will be delivered to `MessageCollectionHandler.onMessagesUpdated()`. + */ +}); + +channel.deleteMessage(message, error => { + messageCollection.deleteMessage(message); +}); + +channel.deleteMessage(message, error => { + /** + * The result will be delivered to `MessageCollectionHandler.onMessagesDeleted()`. + */ +}); + +messageCollection.deleteMessage(failedMessage); + +messageCollection + .removeFailedMessages(failedMessages) + .then(requestIds => { + // Do something... + }) + .catch(error => { + // Handle error. + }); + +messageCollection + .removeAllFailedMessages() + .then(() => { + // Do something... + }) + .catch(error => { + // Handle error. + }); + +messageCollection.resetViewpointTimestamp(TIMESTAMP); + +messageCollection.dispose(); +messageCollection = channel + .createMessageCollection() + .setFilter(messageFilter) + .setStartingPoint(messageCollection.startingPoint) + .setLimit(LIMIT) + .build(); +messageCollection + .initialize(MESSAGE_COLLECTION_INIT_POLICY) + .onCacheResult((error, messages) => { + if (error) { + // Handle error. + } + // Do something... + }) + .onApiResult((error, messages) => { + if (error) { + // Handle error. + } + // Do something... + }); + +messageCollection.fetchPendingMessages(error => { + if (error) { + // Handle error; + } + // Do something... +}); + +const pendingMessages = messageCollection.pendingMessages; + +messageCollection.fetchFailedMessages(error => { + if (error) { + // Handle error; + } + // Do something... +}); + +const failedMessages = messageCollection.failedMessages; + +const messageCount = messageCollection.messageCount; + +const messageCount = messageCollection.succeededMessages.length; + +SendBirdSyncManager.getInstance().clearCache(error => { + if (error) { + // Handle error. + } + // Do something... +}); + +sendbird + .clearCachedMessages(CHANNEL_URLS) + .then(() => { + // Do something... + }) + .catch(error => { + // Handle error. + }); diff --git a/javascript/javascript-basic-local-caching/package.json b/javascript/javascript-basic-local-caching/package.json new file mode 100644 index 00000000..ac1db3c9 --- /dev/null +++ b/javascript/javascript-basic-local-caching/package.json @@ -0,0 +1,39 @@ +{ + "name": "sendbird-sample-javascript-local-caching", + "version": "1.0.0", + "description": "Sendbird Sample using Local Caching", + "main": "index.js", + "scripts": { + "dev": "./node_modules/.bin/webpack --mode=development", + "dev:w": "./node_modules/.bin/webpack --mode=none -w", + "build": "./node_modules/.bin/webpack --mode=production", + "start:dev": "./node_modules/.bin/webpack-dev-server", + "start": "npm run build && node server.js", + "deploy": "node ./deploy/deploy.js" + }, + "author": "Sendbird", + "license": "ISC", + "devDependencies": { + "babel-core": "^6.26.0", + "babel-eslint": "^8.2.3", + "babel-loader": "^7.1.4", + "babel-preset-env": "^1.6.1", + "css-loader": "^0.28.11", + "eslint": "^4.19.1", + "eslint-loader": "^2.1.1", + "express": "^4.16.3", + "extract-text-webpack-plugin": "^4.0.0-beta.0", + "prettier": "^1.14.3", + "sass": "^1.49.9", + "sass-loader": "^10", + "ssh2": "^0.8.5", + "style-loader": "^0.21.0", + "webpack": "^4.19.1", + "webpack-cli": "^3.1.0", + "webpack-dev-server": "^3.1.11" + }, + "dependencies": { + "moment": "^2.22.1", + "sendbird": "^3.1.1" + } +} diff --git a/web-basic-sample-syncmanager/server.js b/javascript/javascript-basic-local-caching/server.js similarity index 100% rename from web-basic-sample-syncmanager/server.js rename to javascript/javascript-basic-local-caching/server.js diff --git a/javascript/javascript-basic-local-caching/src/js/Chat.js b/javascript/javascript-basic-local-caching/src/js/Chat.js new file mode 100644 index 00000000..ee82c7ca --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/Chat.js @@ -0,0 +1,120 @@ +import styles from '../scss/chat.scss'; +import { createDivEl } from './utils'; +import { SendBirdAction } from './SendBirdAction'; +import { SendBirdChatEvent } from './SendBirdChatEvent'; +import { ChatLeftMenu } from './ChatLeftMenu'; +import { ChatTopMenu } from './components/ChatTopMenu'; +import { ChatMain } from './components/ChatMain'; + +let instance = null; + +class Chat { + constructor() { + if (instance) { + return instance; + } + this.body = document.querySelector('.body-center'); + + this.channel = null; + this.element = null; + this.top = null; + this.emptyElement = this._createEmptyElement(); + this.render(); + instance = this; + } + + _createEmptyElement() { + const item = createDivEl({ className: styles['chat-empty'] }); + + const content = createDivEl({ className: styles['empty-content'] }); + item.appendChild(content); + + const title = createDivEl({ className: styles['content-title'], content: 'WELCOME TO SAMPLE CHAT' }); + content.appendChild(title); + const image = createDivEl({ className: styles['content-image'] }); + content.appendChild(image); + const desc = createDivEl({ + className: styles['content-desc'], + content: + 'Create or select a channel to chat in.\n' + + "If you don't have a channel to participate,\n" + + 'go ahead and create your first channel now.' + }); + content.appendChild(desc); + return item; + } + + renderEmptyElement() { + this._removeChatElement(); + this.body.appendChild(this.emptyElement); + } + + _removeEmptyElement() { + if (this.body.contains(this.emptyElement)) { + this.body.removeChild(this.emptyElement); + } + } + + _createChatElement(channel) { + this.element = createDivEl({ className: styles['chat-root'] }); + + this.top = new ChatTopMenu(channel); + this.element.appendChild(this.top.element); + + /// reset manager when ChatMain is obsolete + if (this.main && this.main.body && this.main.body.collection) { + this.main.body.collection.dispose(); + } + this.main = new ChatMain(channel); + } + + _addEventHandler() { + const channelEvent = new SendBirdChatEvent(); + channelEvent.onTypingStatusUpdated = groupChannel => { + if (this.channel.url === groupChannel.url) { + this.main.updateTyping(groupChannel.getTypingMembers()); + } + }; + } + + _renderChatElement(channel) { + const sendbirdAction = SendBirdAction.getInstance(); + this._removeEmptyElement(); + this._removeChatElement(); + this.channel = channel; + + ChatLeftMenu.getInstance().activeChannelItem(channel.url); + this._addEventHandler(); + + sendbirdAction + .getChannel(channel.url) + .then(channel => { + this.channel = channel; + this._createChatElement(this.channel); + this.body.appendChild(this.element); + this.main.loadInitialMessages(); + }) + .catch(() => { + this._createChatElement(this.channel); + this.body.appendChild(this.element); + this.main.loadInitialMessages(); + }); + } + + _removeChatElement() { + const chatElements = this.body.getElementsByClassName(styles['chat-root']); + Array.prototype.slice.call(chatElements).forEach(chatEl => { + chatEl.parentNode.removeChild(chatEl); + }); + } + + render(channel) { + channel ? this._renderChatElement(channel) : this.renderEmptyElement(); + } + + static getInstance() { + return new Chat(); + } +} + +export { Chat }; diff --git a/javascript/javascript-basic-local-caching/src/js/ChatLeftMenu.js b/javascript/javascript-basic-local-caching/src/js/ChatLeftMenu.js new file mode 100644 index 00000000..27707918 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/ChatLeftMenu.js @@ -0,0 +1,144 @@ +import { LeftListItem } from './components/LeftListItem'; +import { ACTIVE_CLASSNAME, DISPLAY_BLOCK, DISPLAY_NONE } from './const'; +import { addClass, isScrollBottom, isUrl, protectFromXSS, removeClass, findChannelIndex } from './utils'; +import { SendBirdAction } from './SendBirdAction'; +import { UserList } from './components/UserList'; +import { Chat } from './Chat'; + +let instance = null; + +class ChatLeftMenu { + constructor() { + if (instance) { + return instance; + } + this.activeChannelUrl = null; + + const action = new SendBirdAction(); + const sb = action.sb; + const groupChannelFilter = new sb.GroupChannelFilter(); + groupChannelFilter.includeEmpty = false; + this.channelCollection = sb.GroupChannel.createGroupChannelCollection() + .setOrder(sb.GroupChannelCollection.GroupChannelOrder.ORDER_LATEST_LAST_MESSAGE) + .setFilter(groupChannelFilter) + .setLimit(50) + .build(); + this.channelCollection.setGroupChannelCollectionHandler({ + onChannelsAdded: (context, channels) => { + for (let i in channels) { + const channel = channels[i]; + const index = findChannelIndex(channel, this.channelCollection.channels); + const handler = () => { + Chat.getInstance().render(channel, false); + this.activeChannelUrl = channel.url; + }; + const item = new LeftListItem({ channel, handler }); + if (index < this.groupChannelList.childNodes.length - 1) { + this.groupChannelList.insertBefore(item.element, this.groupChannelList.childNodes[index]); + } else { + this.groupChannelList.appendChild(item.element); + } + if (this.activeChannelUrl === channel.url) { + this.activeChannelItem(channel.url); + } + } + LeftListItem.updateUnreadCount(); + this.toggleGroupChannelDefaultItem(); + }, + onChannelsUpdated: (context, channels) => { + for (let i in channels) { + const channel = channels[i]; + const item = this.getItem(channel.url); + const handler = () => { + Chat.getInstance().render(channel, false); + this.activeChannelUrl = channel.url; + }; + const newItem = new LeftListItem({ channel, handler }); + this.groupChannelList.replaceChild(newItem.element, item); + if (this.activeChannelUrl === channel.url) { + this.activeChannelItem(channel.url); + } + } + LeftListItem.updateUnreadCount(); + }, + onChannelsDeleted: (context, channelUrls) => { + for (let i in channelUrls) { + const channelUrl = channelUrls[i]; + if (this.activeChannelUrl === channelUrl) { + this.activeChannelUrl = null; + Chat.getInstance().render(); + } + const element = this.getItem(channelUrl); + this.groupChannelList.removeChild(element); + } + this.toggleGroupChannelDefaultItem(); + } + }); + + this.groupChannelList = document.getElementById('group_list'); + this.groupChannelList.addEventListener('scroll', () => { + if (isScrollBottom(this.groupChannelList)) { + this.loadGroupChannelList(); + } + }); + this.groupChannelDefaultItem = document.getElementById('default_item_group'); + + const groupChannelCreateBtn = document.getElementById('group_chat_add'); + groupChannelCreateBtn.addEventListener('click', () => { + UserList.getInstance().render(); + }); + instance = this; + } + + updateUserInfo(user) { + const userInfoEl = document.getElementById('user_info'); + const profileEl = userInfoEl.getElementsByClassName('image-profile')[0]; + if (isUrl(user.profileUrl)) { + profileEl.setAttribute('src', protectFromXSS(user.profileUrl)); + } + const nicknameEl = userInfoEl.getElementsByClassName('nickname-content')[0]; + nicknameEl.innerHTML = protectFromXSS(user.nickname); + } + + loadGroupChannelList() { + this.channelCollection.loadMore().then(channels => { + for (let channel of channels) { + const handler = () => { + Chat.getInstance().render(channel, false); + this.activeChannelUrl = channel.url; + }; + const item = new LeftListItem({ channel, handler }); + this.groupChannelList.appendChild(item.element); + } + this.toggleGroupChannelDefaultItem(); + }); + } + getItem(elementId) { + const groupChannelItems = this.groupChannelList.getElementsByClassName(LeftListItem.getItemRootClassName()); + for (let i = 0; i < groupChannelItems.length; i++) { + if (groupChannelItems[i].id === elementId) { + return groupChannelItems[i]; + } + } + return null; + } + activeChannelItem(channelUrl) { + const groupItems = this.groupChannelList.getElementsByClassName(LeftListItem.getItemRootClassName()); + for (let i = 0; i < groupItems.length; i++) { + groupItems[i].id === channelUrl + ? addClass(groupItems[i], ACTIVE_CLASSNAME) + : removeClass(groupItems[i], ACTIVE_CLASSNAME); + } + } + toggleGroupChannelDefaultItem() { + this.groupChannelList.getElementsByClassName(LeftListItem.getItemRootClassName()).length > 0 + ? (this.groupChannelDefaultItem.style.display = DISPLAY_NONE) + : (this.groupChannelDefaultItem.style.display = DISPLAY_BLOCK); + } + + static getInstance() { + return new ChatLeftMenu(); + } +} + +export { ChatLeftMenu }; diff --git a/javascript/javascript-basic-local-caching/src/js/SendBirdAction.js b/javascript/javascript-basic-local-caching/src/js/SendBirdAction.js new file mode 100644 index 00000000..826bbe31 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/SendBirdAction.js @@ -0,0 +1,237 @@ +import { APP_ID as appId } from './const'; +import { isNull } from './utils'; + +import SendBird from 'sendbird'; + +let instance = null; + +class SendBirdAction { + constructor() { + if (instance) { + return instance; + } + this.sb = new SendBird({ + appId, + localCacheEnabled: true + }); + this.sb.setErrorFirstCallback(true); + this.userQuery = null; + this.groupChannelQuery = null; + this.previousMessageQuery = null; + this.blockedQuery = null; + instance = this; + } + + /** + * Connect + */ + connect(userId, nickname) { + return new Promise((resolve, reject) => { + const sb = SendBird.getInstance(); + sb.connect(userId, (error, user) => { + if (error) { + reject(error); + } else { + sb.updateCurrentUserInfo(decodeURIComponent(nickname), null, (error, user) => { + error ? reject(error) : resolve(user); + }); + } + }); + }); + } + + /** + * User + */ + getCurrentUser() { + return this.sb.currentUser; + } + + getConnectionState() { + return this.sb.getConnectionState(); + } + + /** + * + * #################### SECURITY TIPS #################### + * Before launching, you should review "Allow retrieving user list from SDK" under ⚙️ Sendbird Dashboard ->Settings -> Security. + * It's turned on at first to simplify running samples and implementing your first code. + * Most apps will want to disable "Allow retrieving user list from SDK" as that could possibly expose user information + * #################### SECURITY TIPS #################### + * + */ + getUserList(isInit = false) { + if (isInit || isNull(this.userQuery)) { + this.userQuery = this.sb.createApplicationUserListQuery(); + this.userQuery.limit = 30; + } + return new Promise((resolve, reject) => { + if (this.userQuery.hasNext && !this.userQuery.isLoading) { + this.userQuery.next((error, list) => { + error ? reject(error) : resolve(list); + }); + } else { + resolve([]); + } + }); + } + + isCurrentUser(user) { + return user.userId === this.sb.currentUser.userId; + } + + getBlockedList(isInit = false) { + if (isInit || isNull(this.blockedQuery)) { + this.blockedQuery = this.sb.createBlockedUserListQuery(); + this.blockedQuery.limit = 30; + } + return new Promise((resolve, reject) => { + if (this.blockedQuery.hasNext && !this.blockedQuery.isLoading) { + this.blockedQuery.next((error, blockedList) => { + error ? reject(error) : resolve(blockedList); + }); + } else { + resolve([]); + } + }); + } + + blockUser(user, isBlock = true) { + return new Promise((resolve, reject) => { + if (isBlock) { + this.sb.blockUser(user, (error, response) => { + error ? reject(error) : resolve(); + }); + } else { + this.sb.unblockUser(user, (error, response) => { + error ? reject(error) : resolve(); + }); + } + }); + } + + /** + * Channel + */ + getChannel(channelUrl) { + return new Promise((resolve, reject) => { + this.sb.GroupChannel.getChannel(channelUrl, (error, groupChannel) => { + error ? reject(error) : resolve(groupChannel); + }); + }); + } + + /** + * + * #################### SECURITY TIPS #################### + * Before launching, you should review "Allow creating group channels from SDK" under ⚙️ Sendbird Dashboard -> Settings -> Security. + * It's turned on at first to simplify running samples and implementing your first code. + * Most apps will want to disable "Allow creating group channels from SDK" as that could cause unwanted operations. + * #################### SECURITY TIPS #################### + * + */ + createGroupChannel(userIds) { + return new Promise((resolve, reject) => { + let params = new this.sb.GroupChannelParams(); + params.addUserIds(userIds); + this.sb.GroupChannel.createChannel(params, (error, groupChannel) => { + error ? reject(error) : resolve(groupChannel); + }); + }); + } + + inviteGroupChannel(channelUrl, userIds) { + return new Promise((resolve, reject) => { + this.sb.GroupChannel.getChannel(channelUrl, (error, groupChannel) => { + if (error) { + reject(error); + } else { + groupChannel.inviteWithUserIds(userIds, (error, groupChannel) => { + error ? reject(error) : resolve(groupChannel); + }); + } + }); + }); + } + + leave(channelUrl) { + return new Promise((resolve, reject) => { + this.sb.GroupChannel.getChannel(channelUrl, (error, groupChannel) => { + if (error) { + reject(error); + } else { + groupChannel.leave((error, response) => { + error ? reject(error) : resolve(); + }); + } + }); + }); + } + + hide(channelUrl) { + return new Promise((resolve, reject) => { + this.sb.GroupChannel.getChannel(channelUrl, (error, groupChannel) => { + if (error) { + reject(error); + } else { + groupChannel.hide((error, response) => { + error ? reject(error) : resolve(); + }); + } + }); + }); + } + + markAsRead(channel) { + channel.markAsRead(); + } + + getReadReceipt(channel, message) { + if (this.isCurrentUser(message.sender)) { + return this.sb.currentUser ? channel.getReadReceipt(message) : 0; + } else { + return 0; + } + } + + sendUserMessage({ channel, message, handler }) { + return channel.sendUserMessage(message, (error, message) => { + if (handler) handler(error, message); + }); + } + + sendFileMessage({ channel, file, thumbnailSizes, handler }) { + const fileMessageParams = new this.sb.FileMessageParams(); + fileMessageParams.file = file; + fileMessageParams.thumbnailSizes = thumbnailSizes; + + return channel.sendFileMessage(fileMessageParams, (error, message) => { + if (handler) handler(error, message); + }); + } + + deleteMessage({ channel, message, col }) { + return new Promise((resolve, reject) => { + if (!this.isCurrentUser(message.sender)) { + reject({ + message: 'You have not ownership in this message.' + }); + return; + } + if (message.messageId === 0 && (message.sendingStatus === 'pending' || message.sendingStatus === 'failed')) { + col.deleteMessage(message); + resolve(true); + } else { + channel.deleteMessage(message, (error, response) => { + error ? reject(error) : resolve(response); + }); + } + }); + } + + static getInstance() { + return new SendBirdAction(); + } +} + +export { SendBirdAction }; diff --git a/web-basic-sample-syncmanager/src/js/SendBirdChatEvent.js b/javascript/javascript-basic-local-caching/src/js/SendBirdChatEvent.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/SendBirdChatEvent.js rename to javascript/javascript-basic-local-caching/src/js/SendBirdChatEvent.js diff --git a/web-basic-sample-syncmanager/src/js/SendBirdConnection.js b/javascript/javascript-basic-local-caching/src/js/SendBirdConnection.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/SendBirdConnection.js rename to javascript/javascript-basic-local-caching/src/js/SendBirdConnection.js diff --git a/web-basic-sample-syncmanager/src/js/SendBirdEvent.js b/javascript/javascript-basic-local-caching/src/js/SendBirdEvent.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/SendBirdEvent.js rename to javascript/javascript-basic-local-caching/src/js/SendBirdEvent.js diff --git a/javascript/javascript-basic-local-caching/src/js/components/ChatBody.js b/javascript/javascript-basic-local-caching/src/js/components/ChatBody.js new file mode 100644 index 00000000..e96efcf9 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/components/ChatBody.js @@ -0,0 +1,247 @@ +import styles from '../../scss/chat-body.scss'; +import { createDivEl, getDataInElement, removeClass, findMessageIndex, mergeFailedWithSuccessful } from '../utils'; +import { Message } from './Message'; +import { SendBirdAction } from '../SendBirdAction'; +import { MESSAGE_REQ_ID } from '../const'; +import { Spinner } from './Spinner'; + +class ChatBody { + constructor(channel) { + this.channel = channel; + this.readReceiptManageList = []; + this.scrollHeight = 0; + this.collection = null; + this.limit = 50; + this.element = createDivEl({ className: styles['chat-body'] }); + this._initElement(); + this.spinnerStarted = false; + this.messagesView = []; + } + + _initElement() { + if (this.collection) { + this.collection.dispose(); + } + const action = SendBirdAction.getInstance(); + const sb = action.sb; + const messageFilter = new sb.MessageFilter(); + this.collection = this.channel + .createMessageCollection() + .setFilter(messageFilter) + .setStartingPoint(new Date().getTime()) + .setLimit(this.limit) + .build(); + + this.collection.setMessageCollectionHandler({ + onMessagesAdded: (context, channel, messages) => { + this._mergeMessagesOnInsert(messages); + }, + onMessagesUpdated: (context, channel, messages) => { + this._updateMessages(messages); + }, + onMessagesDeleted: (context, channel, messages) => { + this._removeMessages(messages); + }, + onChannelUpdated: (context, channel) => {}, + onChannelDeleted: (context, channel) => {}, + onHugeGapDetected: () => {} + }); + + this.element.addEventListener('scroll', () => { + if (this.element.scrollTop === 0) { + this.updateCurrentScrollHeight(); + this.collection.loadPrevious().then(messages => { + this.element.scrollTop = this.element.scrollHeight - this.scrollHeight; + }); + } + + if (this.element.scrollHeight - this.element.scrollTop - this.element.clientHeight === 0) { + const newMessagePop = document.getElementById('new-message-pop'); + if (newMessagePop) newMessagePop.remove(); + } + }); + } + + _mergeMessagesOnInsert(messages) { + const { pendingMessages, succeededMessages, failedMessages } = this.collection; + const wholeCollectionMessages = [...pendingMessages, ...succeededMessages, ...failedMessages]; + wholeCollectionMessages.sort((message1, message2) => { + if (message1.messageId !== 0 && message2.messageId !== 0) { + return message1.messageId - message2.messageId; + } else if (message1.reqId && message2.reqId) { + return parseInt(message1.reqId) - parseInt(message2.reqId); + } + return message1.createdAt - message2.createdAt; + }); + for (let message of messages) { + const index = findMessageIndex(message, wholeCollectionMessages); + if (index >= 0) { + const messageElements = this.element.querySelectorAll('.chat-message'); + const messageItem = new Message({ channel: this.channel, message, col: this.collection }); + this.element.insertBefore(messageItem.element, messageElements[index]); + if ( + (message.isUserMessage() || message.isFileMessage()) && + SendBirdAction.getInstance().isCurrentUser(message.sender) + ) { + this.readReceiptManage(message); + } + } + } + } + + _updateMessages(messages, transformToManual = false) { + for (let i in messages) { + const message = messages[i]; + const messageItem = new Message({ + channel: this.channel, + message, + isManual: transformToManual, + col: this.collection + }); + const currentItem = this._getItem(message.reqId); + const requestItem = message.reqId ? this._getItem(message.reqId) : null; + if (currentItem || requestItem) { + this.element.replaceChild(messageItem.element, requestItem ? requestItem : currentItem); + } + } + } + + _removeMessages(messages) { + if ( + this.collection.pendingMessages.length > 0 && + messages.length > 0 && + messages[0].messageId === 0 && + !this.spinnerStarted + ) { + const el = this._getItem(messages[0].reqId); + const resendButton = el.firstChild.getElementsByClassName('resend-button'); + if (resendButton && resendButton.length === 0) { + Spinner.start(this.element); + this.spinnerStarted = true; + } + } + for (let i in messages) { + const message = messages[i]; + this.removeMessage(message.reqId); + } + if ( + (this.spinnerStarted && this.collection.pendingMessages.length === 0) || + SendBirdAction.getInstance().getConnectionState() !== 'OPEN' + ) { + this.stopSpinner(); + } + } + + stopSpinner() { + Spinner.remove(); + this.spinnerStarted = false; + } + + _clearMessages() { + while (this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + } + + loadPreviousMessages(callback) { + this.collection.loadPrevious().then(messages => { + this._mergeMessagesOnInsert(messages); + callback(); + }); + } + + loadInitialMessages(callback) { + const action = SendBirdAction.getInstance(); + const sb = action.sb; + this.collection + .initialize(sb.MessageCollection.MessageCollectionInitPolicy.CACHE_AND_REPLACE_BY_API, new Date().getTime()) + .onCacheResult((error, messages) => { + if (!error) { + this._mergeMessagesOnInsert(messages); + } + }) + .onApiResult((error, messages) => { + if (!error) { + this.element.innerHTML = ''; + this._mergeMessagesOnInsert(messages); + } + callback(); + }); + } + + scrollToBottom() { + this.element.scrollTop = this.element.scrollHeight - this.element.offsetHeight; + } + + updateCurrentScrollHeight() { + this.scrollHeight = this.element.scrollHeight; + } + + repositionScroll(imageOffsetHeight) { + this.element.scrollTop += imageOffsetHeight; + } + + updateReadReceipt() { + this.readReceiptManageList.forEach(message => { + if (message.messageId.toString() !== '0') { + const className = Message.getReadReceiptElementClassName(); + const messageItem = this._getItem(message.reqId); + if (messageItem) { + let readItem = null; + try { + readItem = messageItem.getElementsByClassName(className)[0]; + } catch (e) { + readItem = null; + } + const latestCount = SendBirdAction.getInstance().getReadReceipt(this.channel, message); + if (readItem && latestCount.toString() !== readItem.textContent.toString()) { + readItem.innerHTML = latestCount; + if (latestCount.toString() === '0') { + removeClass(readItem, className); + } + } + } + } + }); + } + + readReceiptManage(message) { + for (let i = 0; i < this.readReceiptManageList.length; i++) { + if (message.reqId) { + if (this.readReceiptManageList[i].reqId === message.reqId) { + this.readReceiptManageList.splice(i, 1); + break; + } + } else { + if (this.readReceiptManageList[i].messageId === message.messageId) { + this.readReceiptManageList.splice(i, 1); + break; + } + } + } + this.readReceiptManageList.push(message); + this.updateReadReceipt(); + } + + _getItem(reqId) { + const items = this.element.childNodes; + // We go in reverse order to prevent situations that + // pending-message remove requests accidentally delete succeeded messages + for (let i = items.length - 1; i >= 0; i--) { + const elementId = getDataInElement(items[i], MESSAGE_REQ_ID); + if (elementId === reqId.toString()) { + return items[i]; + } + } + return null; + } + + removeMessage(reqId) { + const removeElement = this._getItem(reqId); + if (removeElement) { + this.element.removeChild(removeElement); + } + } +} + +export { ChatBody }; diff --git a/javascript/javascript-basic-local-caching/src/js/components/ChatInput.js b/javascript/javascript-basic-local-caching/src/js/components/ChatInput.js new file mode 100644 index 00000000..da23c486 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/components/ChatInput.js @@ -0,0 +1,116 @@ +import styles from '../../scss/chat-input.scss'; +import { createDivEl, protectFromXSS } from '../utils'; +import { DISPLAY_BLOCK, DISPLAY_NONE, FILE_ID, KEY_ENTER } from '../const'; +import { SendBirdAction } from '../SendBirdAction'; +import { Chat } from '../Chat'; + +class ChatInput { + constructor(channel) { + this.channel = channel; + this.input = null; + this.typing = null; + this.element = this._createElement(channel); + } + + _createElement(channel) { + const sendbirdAction = SendBirdAction.getInstance(); + const chat = Chat.getInstance(); + const root = createDivEl({ className: styles['chat-input'] }); + + this.typing = createDivEl({ className: styles['typing-field'] }); + root.appendChild(this.typing); + + const file = document.createElement('label'); + file.className = styles['input-file']; + file.for = FILE_ID; + file.addEventListener('click', () => { + sendbirdAction.markAsRead(this.channel); + }); + + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = FILE_ID; + fileInput.style.display = DISPLAY_NONE; + fileInput.addEventListener('change', () => { + const sendFile = fileInput.files[0]; + if (sendFile) { + const previewMessage = SendBirdAction.getInstance().sendFileMessage({ + channel: this.channel, + file: sendFile, + thumbnailSizes: [ + { maxWidth: 240, maxHeight: 240 }, + { maxWidth: 320, maxHeight: 320 } + ], + handler: (error, message) => { + chat.main.body.scrollToBottom(); + } + }); + previewMessage.createdAt = new Date().getTime(); + } + }); + + file.appendChild(fileInput); + root.appendChild(file); + + const inputText = createDivEl({ className: styles['input-text'] }); + + this.input = document.createElement('textarea'); + this.input.className = styles['input-text-area']; + this.input.placeholder = 'Write a chat...'; + this.input.addEventListener('click', () => { + sendbirdAction.markAsRead(this.channel); + }); + this.input.addEventListener('keypress', e => { + if (e.keyCode === KEY_ENTER) { + if (!e.shiftKey) { + e.preventDefault(); + const message = this.input.value; + this.input.value = ''; + if (message) { + const previewMessage = SendBirdAction.getInstance().sendUserMessage({ + channel: this.channel, + message, + handler: (error, message) => { + chat.main.body.scrollToBottom(); + } + }); + channel.endTyping(); + } + } else { + channel.startTyping(); + } + } else { + channel.startTyping(); + } + }); + this.input.addEventListener('focusin', () => { + inputText.style.border = '1px solid #2C2D30'; + }); + this.input.addEventListener('focusout', () => { + inputText.style.border = ''; + }); + + inputText.appendChild(this.input); + root.appendChild(inputText); + return root; + } + + updateTyping(memberList) { + let nicknames = ''; + if (memberList.length === 1) { + nicknames = `${protectFromXSS(memberList[0].nickname)} is`; + } else if (memberList.length === 2) { + nicknames = `${memberList + .map(member => { + return protectFromXSS(member.nickname); + }) + .join(', ')} are`; + } else if (memberList.length !== 0) { + nicknames = 'Several are'; + } + this.typing.style.display = nicknames ? DISPLAY_BLOCK : DISPLAY_NONE; + this.typing.innerHTML = `${nicknames} typing...`; + } +} + +export { ChatInput }; diff --git a/javascript/javascript-basic-local-caching/src/js/components/ChatMain.js b/javascript/javascript-basic-local-caching/src/js/components/ChatMain.js new file mode 100644 index 00000000..8d323d98 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/components/ChatMain.js @@ -0,0 +1,59 @@ +import styles from '../../scss/chat-main.scss'; +import { ChatBody } from './ChatBody'; +import { ChatInput } from './ChatInput'; +import { Chat } from '../Chat'; +import { createDivEl } from '../utils'; +import { ChatMenu } from './ChatMenu'; +import { SendBirdAction } from '../SendBirdAction'; + +class ChatMain { + constructor(channel) { + this.channel = channel; + this.body = null; + this.input = null; + this.menu = null; + this._create(); + } + + _create() { + const root = createDivEl({ className: styles['chat-main-root'] }); + + const main = createDivEl({ className: styles['chat-main'] }); + root.appendChild(main); + + this.body = new ChatBody(this.channel); + main.appendChild(this.body.element); + + this.input = new ChatInput(this.channel); + main.appendChild(this.input.element); + + this.menu = new ChatMenu(this.channel); + root.appendChild(this.menu.element); + + Chat.getInstance().element.appendChild(root); + } + + updateTyping(memberList) { + this.input.updateTyping(memberList); + } + + repositionScroll(height) { + this.body.repositionScroll(height); + } + + updateBlockedList(user, isBlock) { + this.menu.updateBlockedList(user, isBlock); + } + + loadInitialMessages() { + const sendbirdAction = SendBirdAction.getInstance(); + this.body.loadInitialMessages(() => { + this.body.loadPreviousMessages(() => { + sendbirdAction.markAsRead(this.channel); + this.body.scrollToBottom(); + }); + }); + } +} + +export { ChatMain }; diff --git a/web-basic-sample-syncmanager/src/js/components/ChatMenu.js b/javascript/javascript-basic-local-caching/src/js/components/ChatMenu.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/ChatMenu.js rename to javascript/javascript-basic-local-caching/src/js/components/ChatMenu.js diff --git a/web-basic-sample-syncmanager/src/js/components/ChatTopMenu.js b/javascript/javascript-basic-local-caching/src/js/components/ChatTopMenu.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/ChatTopMenu.js rename to javascript/javascript-basic-local-caching/src/js/components/ChatTopMenu.js diff --git a/web-basic-sample-syncmanager/src/js/components/ChatUserItem.js b/javascript/javascript-basic-local-caching/src/js/components/ChatUserItem.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/ChatUserItem.js rename to javascript/javascript-basic-local-caching/src/js/components/ChatUserItem.js diff --git a/web-basic-sample-syncmanager/src/js/components/LeftListItem.js b/javascript/javascript-basic-local-caching/src/js/components/LeftListItem.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/LeftListItem.js rename to javascript/javascript-basic-local-caching/src/js/components/LeftListItem.js diff --git a/web-basic-sample-syncmanager/src/js/components/List.js b/javascript/javascript-basic-local-caching/src/js/components/List.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/List.js rename to javascript/javascript-basic-local-caching/src/js/components/List.js diff --git a/javascript/javascript-basic-local-caching/src/js/components/Message.js b/javascript/javascript-basic-local-caching/src/js/components/Message.js new file mode 100644 index 00000000..ec32cd31 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/components/Message.js @@ -0,0 +1,249 @@ +import styles from '../../scss/message.scss'; +import { createDivEl, isImage, protectFromXSS, setDataInElement, timestampToTime } from '../utils'; +import { SendBirdAction } from '../SendBirdAction'; +import { COLOR_RED, MESSAGE_REQ_ID } from '../const'; +import { MessageDeleteModal } from './MessageDeleteModal'; +import { UserBlockModal } from './UserBlockModal'; +import { Chat } from '../Chat'; + +class Message { + constructor({ channel, message, isManual = false, col = null }) { + this.channel = channel; + this.message = message; + this.isPending = message.messageId === 0 && message.sendingStatus === 'pending'; + this.isFailed = message.messageId === 0 && message.sendingStatus === 'failed'; + this.isManual = (this.isPending || this.isFailed) ? isManual : false; + this.element = this._createElement(); + if (col) { + this.col = col; + } + } + + _createElement() { + if (this.message.isUserMessage()) { + return this._createUserElement(); + } else if (this.message.isFileMessage()) { + return this._createFileElement(); + } else if (this.message.isAdminMessage()) { + return this._createAdminElement(); + } else { + // console.error('Message is invalid data.'); + return null; + } + } + + _hoverOnNickname(nickname, hover) { + if (!SendBirdAction.getInstance().isCurrentUser(this.message.sender)) { + nickname.innerHTML = hover ? 'BLOCK ' : `${protectFromXSS(this.message.sender.nickname)} : `; + nickname.style.color = hover ? COLOR_RED : ''; + nickname.style.opacity = hover ? '1' : ''; + } + } + + _hoverOnTime(time, hover) { + if (SendBirdAction.getInstance().isCurrentUser(this.message.sender)) { + time.innerHTML = hover ? 'DELETE' : timestampToTime(this.message.createdAt); + time.style.color = hover ? COLOR_RED : ''; + time.style.opacity = hover ? '1' : ''; + time.style.fontWeight = hover ? '600' : ''; + } + } + + _createUserElement() { + const sendbirdAction = SendBirdAction.getInstance(); + const isCurrentUser = sendbirdAction.isCurrentUser(this.message.sender); + let root; + if (this.isFailed && !this.isManual) { + root = createDivEl({ + className: [styles['chat-message'], styles['is-failed']], + id: this.message.reqId + }); + } else if (this.isPending && !this.isManual) { + root = createDivEl({ + className: [styles['chat-message'], styles['is-pending']], + id: this.message.reqId + }); + } else { + root = createDivEl({ + className: styles['chat-message'], + id: this.message.reqId + }); + } + setDataInElement(root, MESSAGE_REQ_ID, this.message.reqId); + + const messageContent = createDivEl({ className: styles['message-content'] }); + const nickname = createDivEl({ + className: isCurrentUser ? [styles['message-nickname'], styles['is-user']] : styles['message-nickname'], + content: `${protectFromXSS(this.message.sender.nickname)} : ` + }); + nickname.addEventListener('mouseover', () => { + this._hoverOnNickname(nickname, true); + }); + nickname.addEventListener('mouseleave', () => { + this._hoverOnNickname(nickname, false); + }); + nickname.addEventListener('click', () => { + if (!isCurrentUser) { + const userBlockModal = new UserBlockModal({ user: this.message.sender, isBlock: true }); + userBlockModal.render(); + } + }); + messageContent.appendChild(nickname); + + const msg = createDivEl({ className: styles['message-content'], content: protectFromXSS(this.message.message) }); + messageContent.appendChild(msg); + + if (this.isFailed && this.isManual) { + const resendButton = createDivEl({ + className: styles['resend-button'], + content: 'RESEND' + }); + resendButton.addEventListener('click', () => { + this._resendUserMessage(); + }); + messageContent.appendChild(resendButton); + } + if (this.isFailed) { + const deleteButton = createDivEl({ + className: styles['delete-button'], + content: 'DELETE' + }); + deleteButton.addEventListener('click', () => { + if (isCurrentUser) { + const messageDeleteModal = new MessageDeleteModal({ + channel: this.channel, + message: this.message, + col: this.col + }); + messageDeleteModal.render(); + } + }); + messageContent.appendChild(deleteButton); + } + if (!this.isFailed) { + const time = createDivEl({ + className: isCurrentUser ? [styles.time, styles['is-user']] : styles.time, + content: timestampToTime(this.message.createdAt) + }); + time.addEventListener('mouseover', () => { + this._hoverOnTime(time, true); + }); + time.addEventListener('mouseleave', () => { + this._hoverOnTime(time, false); + }); + time.addEventListener('click', () => { + if (isCurrentUser) { + const messageDeleteModal = new MessageDeleteModal({ + channel: this.channel, + message: this.message + }); + messageDeleteModal.render(); + } + }); + messageContent.appendChild(time); + + const count = sendbirdAction.getReadReceipt(this.channel, this.message); + const read = createDivEl({ + className: count ? [styles.read, styles.active] : styles.read, + content: count + }); + messageContent.appendChild(read); + } + + root.appendChild(messageContent); + return root; + } + + _resendUserMessage() { + this.channel.resendUserMessage(this.message, (error, message) => { }); + } + + _createFileElement() { + const sendbirdAction = SendBirdAction.getInstance(); + const root = createDivEl({ className: styles['chat-message'], id: this.message.messageId }); + setDataInElement(root, MESSAGE_REQ_ID, this.message.reqId); + + const messageContent = createDivEl({ className: styles['message-content'] }); + const nickname = createDivEl({ + className: sendbirdAction.isCurrentUser(this.message.sender) + ? [styles['message-nickname'], styles['is-user']] + : styles['message-nickname'], + content: `${protectFromXSS(this.message.sender.nickname)} : ` + }); + messageContent.appendChild(nickname); + + const msg = createDivEl({ + className: [styles['message-content'], styles['is-file']], + content: protectFromXSS(this.message.name) + }); + msg.addEventListener('click', () => { + window.open(this.message.url); + }); + messageContent.appendChild(msg); + + const time = createDivEl({ className: styles.time, content: timestampToTime(this.message.createdAt) }); + time.addEventListener('mouseover', () => { + this._hoverOnTime(time, true); + }); + time.addEventListener('mouseleave', () => { + this._hoverOnTime(time, false); + }); + time.addEventListener('click', () => { + const messageDeleteModal = new MessageDeleteModal({ + channel: this.channel, + message: this.message + }); + messageDeleteModal.render(); + }); + messageContent.appendChild(time); + + if (this.channel.isGroupChannel()) { + const count = sendbirdAction.getReadReceipt(this.channel, this.message); + const read = createDivEl({ + className: count ? [styles.read, styles.active] : styles.read, + content: count + }); + messageContent.appendChild(read); + } + + root.appendChild(messageContent); + + if (this.message.isFileMessage() && this.message.messageId) { + const fileContent = createDivEl({ className: styles['file-content'] }); + fileContent.addEventListener('click', () => { + window.open(this.message.url); + }); + if (this.message.thumbnails.length > 0 || isImage(this.message.type)) { + const fileRender = document.createElement('img'); + fileRender.className = styles['file-render']; + + if (isImage(this.message.type)) { + fileRender.src = protectFromXSS(this.message.url); + } else if (this.message.thumbnails.length > 0) { + fileRender.src = protectFromXSS(this.message.thumbnails[0].url); + } + + fileRender.onload = () => { + Chat.getInstance().main.repositionScroll(fileRender.offsetHeight); + }; + fileContent.appendChild(fileRender); + } + root.appendChild(fileContent); + } + + return root; + } + + _createAdminElement() { + const root = createDivEl({ className: styles['chat-message'], id: this.message.messageId }); + const msg = createDivEl({ className: styles['message-admin'], content: protectFromXSS(this.message.message) }); + root.appendChild(msg); + return root; + } + + static getReadReceiptElementClassName() { + return styles.active; + } +} + +export { Message }; diff --git a/web-basic-sample-syncmanager/src/js/components/MessageDeleteModal.js b/javascript/javascript-basic-local-caching/src/js/components/MessageDeleteModal.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/MessageDeleteModal.js rename to javascript/javascript-basic-local-caching/src/js/components/MessageDeleteModal.js diff --git a/web-basic-sample-syncmanager/src/js/components/Modal.js b/javascript/javascript-basic-local-caching/src/js/components/Modal.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/Modal.js rename to javascript/javascript-basic-local-caching/src/js/components/Modal.js diff --git a/web-basic-sample-syncmanager/src/js/components/Spinner.js b/javascript/javascript-basic-local-caching/src/js/components/Spinner.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/Spinner.js rename to javascript/javascript-basic-local-caching/src/js/components/Spinner.js diff --git a/web-basic-sample-syncmanager/src/js/components/Toast.js b/javascript/javascript-basic-local-caching/src/js/components/Toast.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/Toast.js rename to javascript/javascript-basic-local-caching/src/js/components/Toast.js diff --git a/web-basic-sample-syncmanager/src/js/components/UserBlockModal.js b/javascript/javascript-basic-local-caching/src/js/components/UserBlockModal.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/UserBlockModal.js rename to javascript/javascript-basic-local-caching/src/js/components/UserBlockModal.js diff --git a/web-basic-sample-syncmanager/src/js/components/UserItem.js b/javascript/javascript-basic-local-caching/src/js/components/UserItem.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/UserItem.js rename to javascript/javascript-basic-local-caching/src/js/components/UserItem.js diff --git a/web-basic-sample-syncmanager/src/js/components/UserList.js b/javascript/javascript-basic-local-caching/src/js/components/UserList.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/components/UserList.js rename to javascript/javascript-basic-local-caching/src/js/components/UserList.js diff --git a/web-basic-sample-syncmanager/src/js/const.js b/javascript/javascript-basic-local-caching/src/js/const.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/const.js rename to javascript/javascript-basic-local-caching/src/js/const.js diff --git a/web-basic-sample-syncmanager/src/js/index.js b/javascript/javascript-basic-local-caching/src/js/index.js similarity index 100% rename from web-basic-sample-syncmanager/src/js/index.js rename to javascript/javascript-basic-local-caching/src/js/index.js diff --git a/javascript/javascript-basic-local-caching/src/js/main.js b/javascript/javascript-basic-local-caching/src/js/main.js new file mode 100644 index 00000000..d75ea5ad --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/main.js @@ -0,0 +1,55 @@ +import { getVariableFromUrl, isEmpty, redirectToIndex } from './utils'; +import { SendBirdAction } from './SendBirdAction'; +import { SendBirdConnection } from './SendBirdConnection'; +import { ChatLeftMenu } from './ChatLeftMenu'; +import { Chat } from './Chat'; +import { UPDATE_INTERVAL_TIME } from './const'; +import { LeftListItem } from './components/LeftListItem'; + +import { Toast } from './components/Toast'; + +const sb = new SendBirdAction(); + +let chat = null; +let chatLeft = null; + +const createConnectionHandler = () => { + const connectionManager = new SendBirdConnection(); + connectionManager.onReconnectStarted = () => { + Toast.start(document.body, 'Connection is lost. Trying to reconnect...'); + connectionManager.channel = chat.channel; + }; + connectionManager.onReconnectSucceeded = () => { + chatLeft.updateUserInfo(SendBirdAction.getInstance().getCurrentUser()); + Toast.remove(); + }; + connectionManager.onReconnectFailed = () => { + connectionManager.reconnect(); + }; +}; + +const updateGroupChannelTime = () => { + setInterval(() => { + LeftListItem.updateLastMessageTime(); + }, UPDATE_INTERVAL_TIME); +}; + +document.addEventListener('DOMContentLoaded', () => { + const { userid, nickname } = getVariableFromUrl(); + if (isEmpty(userid) || isEmpty(nickname)) { + redirectToIndex('UserID and Nickname must be required.'); + } + + sb.connect(userid, nickname) + .then(user => { + chat = new Chat(); + chatLeft = new ChatLeftMenu(); + chatLeft.updateUserInfo(user); + updateGroupChannelTime(); + chatLeft.loadGroupChannelList(true); + createConnectionHandler(); + }) + .catch(() => { + Toast.start(document.body, 'Connection is not established.'); + }); +}); diff --git a/javascript/javascript-basic-local-caching/src/js/utils.js b/javascript/javascript-basic-local-caching/src/js/utils.js new file mode 100644 index 00000000..3e94efc2 --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/js/utils.js @@ -0,0 +1,231 @@ +import moment from 'moment'; + +export function findChannelIndex(newChannel, channels) { + const newChannelLastMessageUpdated = newChannel.lastMessage ? newChannel.lastMessage.createdAt : newChannel.createdAt; + + let index = channels.length; + for (let i = 0; i < channels.length; i++) { + const comparedChannel = channels[i]; + const comparedChannelLastMessageUpdated = comparedChannel.lastMessage + ? comparedChannel.lastMessage.createdAt + : comparedChannel.createdAt; + if (newChannel.url === comparedChannel.url) { + index = i; + break; + } else if (newChannelLastMessageUpdated > comparedChannelLastMessageUpdated) { + index = i; + break; + } + } + return index; +} +export function findMessageIndex(newMessage, messages, isRequestId = false) { + let index = messages.length; + for (let i = 0; i < messages.length; i++) { + if ( + !isRequestId && + newMessage.messageId !== 0 && + messages[i].messageId !== 0 && + messages[i].messageId === newMessage.messageId + ) { + index = i; + break; + } else if (isRequestId && messages[i].reqId === newMessage.reqId) { + index = i; + break; + } else if (messages[i].createdAt >= newMessage.createdAt) { + index = i; + break; + } + } + return index; +} + +export function mergeFailedWithSuccessful(failedMessages, successfulMessages) { + const wholeMessages = [...successfulMessages]; + for (let i = 0; i < failedMessages.length; i++) { + const index = findMessageIndex(failedMessages[i], wholeMessages); + wholeMessages.splice(index, 0, failedMessages[i]); + } + return wholeMessages; +} + +export const timestampToTime = timestamp => { + const now = new Date().getTime(); + const nowDate = moment.unix(now.toString().length === 13 ? now / 1000 : now).format('MM/DD'); + + let date = moment.unix(timestamp.toString().length === 13 ? timestamp / 1000 : timestamp).format('MM/DD'); + if (date === 'Invalid date') { + date = ''; + } + + return nowDate === date + ? moment.unix(timestamp.toString().length === 13 ? timestamp / 1000 : timestamp).format('HH:mm') + : date; +}; + +export const timestampToDateString = timestamp => { + return moment.unix(timestamp.toString().length === 13 ? timestamp / 1000 : timestamp).format('LL'); +}; + +export const timestampFromNow = timestamp => { + return moment(timestamp).fromNow(); +}; + +export const isUrl = urlString => { + const regex = /^(http|https):\/\/[^ "]+$/; + return regex.test(urlString); +}; + +export const isImage = fileType => { + const regex = /^image\/.+$/; + return regex.test(fileType); +}; + +export const isEmpty = value => { + return value === null || value === undefined || value.length === 0; +}; + +export const isNull = value => { + try { + return value === null; + } catch (e) { + return false; + } +}; + +export const setCookie = (key, value) => { + document.cookie = `${key}=${value}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; +}; + +export const getCookie = key => { + let name = `${key}=`; + let ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + if (!c) continue; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ''; +}; + +export const getVariableFromUrl = () => { + let vars = {}; + let hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); + for (let i = 0; i < hashes.length; i++) { + let hash = hashes[i].split('='); + vars[hash[0]] = hash[1]; + } + return vars; +}; + +export const errorAlert = (message, reload = false) => { + // alert(message); + // eslint-disable-next-line no-console + console.error(message); + if (reload) { + location.reload(true); + } +}; + +export const redirectToIndex = message => { + if (message) { + errorAlert(message, false); + } + window.location.href = 'index.html'; +}; + +export const setDataInElement = (target, key, data) => { + target.dataset[`${key}`] = data; +}; + +export const getDataInElement = (target, key) => { + return target.dataset[`${key}`]; +}; + +export const createDivEl = ({ id, className, content, background }) => { + const el = document.createElement('div'); + if (id) { + el.id = id; + } + if (className) { + el.className = Array.isArray(className) ? className.join(' ') : className; + } + if (content) { + el.innerHTML = content; + } + if (background) { + el.style.backgroundImage = `url(${background})`; + } + return el; +}; + +export const isScrollBottom = target => { + return target.scrollTop + target.offsetHeight >= target.scrollHeight; +}; + +export const appendToFirst = (target, newElement) => { + if (target.childNodes.length > 0) { + target.insertBefore(newElement, target.childNodes[0]); + } else { + target.appendChild(newElement); + } +}; + +const hasClass = (target, className) => { + return target.classList + ? target.classList.contains(className) + : new RegExp('(^| )' + className + '( |$)', 'gi').test(target.className); +}; + +export const addClass = (target, className) => { + if (target.classList) { + if (!(className in target.classList)) { + target.classList.add(className); + } + } else { + if (target.className.indexOf(className) < 0) { + target.className += ` ${className}`; + } + } +}; + +export const removeClass = (target, className) => { + if (target.classList) { + target.classList.remove(className); + } else { + target.className = target.className.replace( + new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), + '' + ); + } +}; + +export const toggleClass = (target, className) => { + hasClass(target, className) ? removeClass(target, className) : addClass(target, className); +}; + +export const uuid4 = () => { + let d = new Date().getTime(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); + }); +}; + +export const protectFromXSS = text => { + return typeof text === 'string' + ? text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + : text; +}; diff --git a/web-basic-sample-syncmanager/src/scss/_animation.scss b/javascript/javascript-basic-local-caching/src/scss/_animation.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/_animation.scss rename to javascript/javascript-basic-local-caching/src/scss/_animation.scss diff --git a/web-basic-sample-syncmanager/src/scss/_common.scss b/javascript/javascript-basic-local-caching/src/scss/_common.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/_common.scss rename to javascript/javascript-basic-local-caching/src/scss/_common.scss diff --git a/web-basic-sample-syncmanager/src/scss/_icons.scss b/javascript/javascript-basic-local-caching/src/scss/_icons.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/_icons.scss rename to javascript/javascript-basic-local-caching/src/scss/_icons.scss diff --git a/web-basic-sample-syncmanager/src/scss/_mixins.scss b/javascript/javascript-basic-local-caching/src/scss/_mixins.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/_mixins.scss rename to javascript/javascript-basic-local-caching/src/scss/_mixins.scss diff --git a/web-basic-sample-syncmanager/src/scss/_normalize.scss b/javascript/javascript-basic-local-caching/src/scss/_normalize.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/_normalize.scss rename to javascript/javascript-basic-local-caching/src/scss/_normalize.scss diff --git a/web-basic-sample-syncmanager/src/scss/_variables.scss b/javascript/javascript-basic-local-caching/src/scss/_variables.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/_variables.scss rename to javascript/javascript-basic-local-caching/src/scss/_variables.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat-body.scss b/javascript/javascript-basic-local-caching/src/scss/chat-body.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat-body.scss rename to javascript/javascript-basic-local-caching/src/scss/chat-body.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat-input.scss b/javascript/javascript-basic-local-caching/src/scss/chat-input.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat-input.scss rename to javascript/javascript-basic-local-caching/src/scss/chat-input.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat-main.scss b/javascript/javascript-basic-local-caching/src/scss/chat-main.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat-main.scss rename to javascript/javascript-basic-local-caching/src/scss/chat-main.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat-menu.scss b/javascript/javascript-basic-local-caching/src/scss/chat-menu.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat-menu.scss rename to javascript/javascript-basic-local-caching/src/scss/chat-menu.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat-top-menu.scss b/javascript/javascript-basic-local-caching/src/scss/chat-top-menu.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat-top-menu.scss rename to javascript/javascript-basic-local-caching/src/scss/chat-top-menu.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat-user-item.scss b/javascript/javascript-basic-local-caching/src/scss/chat-user-item.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat-user-item.scss rename to javascript/javascript-basic-local-caching/src/scss/chat-user-item.scss diff --git a/web-basic-sample-syncmanager/src/scss/chat.scss b/javascript/javascript-basic-local-caching/src/scss/chat.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/chat.scss rename to javascript/javascript-basic-local-caching/src/scss/chat.scss diff --git a/web-basic-sample-syncmanager/src/scss/index.scss b/javascript/javascript-basic-local-caching/src/scss/index.scss similarity index 99% rename from web-basic-sample-syncmanager/src/scss/index.scss rename to javascript/javascript-basic-local-caching/src/scss/index.scss index 76a4242f..c504b226 100644 --- a/web-basic-sample-syncmanager/src/scss/index.scss +++ b/javascript/javascript-basic-local-caching/src/scss/index.scss @@ -38,7 +38,6 @@ body { &>.logo-image { display: flex; align-items: center; - margin-left: 10px; } } diff --git a/web-basic-sample-syncmanager/src/scss/list-item.scss b/javascript/javascript-basic-local-caching/src/scss/list-item.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/list-item.scss rename to javascript/javascript-basic-local-caching/src/scss/list-item.scss diff --git a/web-basic-sample-syncmanager/src/scss/list.scss b/javascript/javascript-basic-local-caching/src/scss/list.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/list.scss rename to javascript/javascript-basic-local-caching/src/scss/list.scss diff --git a/web-basic-sample-syncmanager/src/scss/main.scss b/javascript/javascript-basic-local-caching/src/scss/main.scss similarity index 99% rename from web-basic-sample-syncmanager/src/scss/main.scss rename to javascript/javascript-basic-local-caching/src/scss/main.scss index e5c2ac1d..bc07b2e2 100644 --- a/web-basic-sample-syncmanager/src/scss/main.scss +++ b/javascript/javascript-basic-local-caching/src/scss/main.scss @@ -33,7 +33,6 @@ &>.logo-image { display: flex; align-items: center; - margin-left: 5px; } } diff --git a/web-basic-sample-syncmanager/src/scss/message-delete-modal.scss b/javascript/javascript-basic-local-caching/src/scss/message-delete-modal.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/message-delete-modal.scss rename to javascript/javascript-basic-local-caching/src/scss/message-delete-modal.scss diff --git a/javascript/javascript-basic-local-caching/src/scss/message.scss b/javascript/javascript-basic-local-caching/src/scss/message.scss new file mode 100644 index 00000000..e3f6d3ab --- /dev/null +++ b/javascript/javascript-basic-local-caching/src/scss/message.scss @@ -0,0 +1,113 @@ +@import 'mixins'; +@import 'variables'; +@import 'icons'; + +.chat-message { + display: block; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + box-sizing: border-box; + padding: 0 20px; + margin-bottom: 8px; + border: 1px solid transparent; + + & > .message-content { + display: inline; + & > .message-nickname { + align-items: center; + display: inline; + justify-content: flex-start; + flex-direction: column; + cursor: pointer; + } + & > .message-nickname.is-user { + font-weight: 600; + color: $color-purple-deep; + cursor: initial; + } + + & > .message-content { + display: inline; + white-space: pre-line; + } + & > .message-content.is-file { + cursor: pointer; + @include hover-focus { + color: $color-blue-dark; + } + } + + & > .time { + display: inline; + margin-left: 8px; + font-size: 12px; + opacity: 0.5; + } + & > .time.is-user { + cursor: pointer; + } + + & > .read { + display: none; + vertical-align: middle; + text-align: center; + width: 18px; + height: 18px; + line-height: 17px; + margin-left: 8px; + font-size: 12px; + color: $color-white; + font-weight: 500; + @include border-radius(50%); + background: $color-red; + } + & > .read.active { + display: inline-block; + } + & > .resend-button { + display: inline; + margin-left: 8px; + font-size: 12px; + cursor: pointer; + color: $color-red; + } + + & > .delete-button { + display: inline; + margin-left: 8px; + font-size: 12px; + cursor: pointer; + color: $color-red; + } + } + + & > .file-content { + display: block; + border-left: 2px solid $color-black-text; + padding-left: 10px; + margin-top: 8px; + cursor: pointer; + & > .file-render { + display: inline; + max-width: 300px; + max-height: 300px; + } + } + + & > .message-admin { + display: flex; + align-items: center; + width: 100%; + font-style: italic; + color: $color-black-text; + } +} + +.chat-message.is-failed { + background: $color-message-not-sent; +} +.chat-message.is-pending { + color: $color-message-not-sent; +} diff --git a/web-basic-sample-syncmanager/src/scss/mixins/_border-radius.scss b/javascript/javascript-basic-local-caching/src/scss/mixins/_border-radius.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/mixins/_border-radius.scss rename to javascript/javascript-basic-local-caching/src/scss/mixins/_border-radius.scss diff --git a/web-basic-sample-syncmanager/src/scss/mixins/_reset.scss b/javascript/javascript-basic-local-caching/src/scss/mixins/_reset.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/mixins/_reset.scss rename to javascript/javascript-basic-local-caching/src/scss/mixins/_reset.scss diff --git a/web-basic-sample-syncmanager/src/scss/mixins/_state.scss b/javascript/javascript-basic-local-caching/src/scss/mixins/_state.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/mixins/_state.scss rename to javascript/javascript-basic-local-caching/src/scss/mixins/_state.scss diff --git a/web-basic-sample-syncmanager/src/scss/mixins/_transform.scss b/javascript/javascript-basic-local-caching/src/scss/mixins/_transform.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/mixins/_transform.scss rename to javascript/javascript-basic-local-caching/src/scss/mixins/_transform.scss diff --git a/web-basic-sample-syncmanager/src/scss/modal.scss b/javascript/javascript-basic-local-caching/src/scss/modal.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/modal.scss rename to javascript/javascript-basic-local-caching/src/scss/modal.scss diff --git a/web-basic-sample-syncmanager/src/scss/spinner.scss b/javascript/javascript-basic-local-caching/src/scss/spinner.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/spinner.scss rename to javascript/javascript-basic-local-caching/src/scss/spinner.scss diff --git a/web-basic-sample-syncmanager/src/scss/toast.scss b/javascript/javascript-basic-local-caching/src/scss/toast.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/toast.scss rename to javascript/javascript-basic-local-caching/src/scss/toast.scss diff --git a/web-basic-sample-syncmanager/src/scss/user-block-modal.scss b/javascript/javascript-basic-local-caching/src/scss/user-block-modal.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/user-block-modal.scss rename to javascript/javascript-basic-local-caching/src/scss/user-block-modal.scss diff --git a/web-basic-sample-syncmanager/src/scss/user-item.scss b/javascript/javascript-basic-local-caching/src/scss/user-item.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/user-item.scss rename to javascript/javascript-basic-local-caching/src/scss/user-item.scss diff --git a/web-basic-sample-syncmanager/src/scss/user-list.scss b/javascript/javascript-basic-local-caching/src/scss/user-list.scss similarity index 100% rename from web-basic-sample-syncmanager/src/scss/user-list.scss rename to javascript/javascript-basic-local-caching/src/scss/user-list.scss diff --git a/javascript/javascript-basic-local-caching/webpack.config.js b/javascript/javascript-basic-local-caching/webpack.config.js new file mode 100644 index 00000000..a33ab0a6 --- /dev/null +++ b/javascript/javascript-basic-local-caching/webpack.config.js @@ -0,0 +1,79 @@ +'use strict'; +const path = require('path'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const PRODUCTION = 'production'; + +module.exports = () => { + const config = { + mode: 'production', + entry: { + index: ['./src/js/index.js', './src/scss/index.scss'], + main: ['./src/js/main.js', './src/scss/main.scss'] + }, + output: { + path: path.resolve(__dirname, './dist'), + filename: 'sample.[name].js', + library: '[name]', + libraryExport: 'default', + libraryTarget: 'umd', + publicPath: 'dist' + }, + devtool: 'cheap-eval-source-map', + devServer: { + publicPath: '/dist/', + compress: true, + port: 9000 + }, + performance: { hints: false }, + module: { + rules: [ + { + // SCSS/SASS + test: /\.s[ac]ss$/i, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: [ + { + loader: 'css-loader', + options: { + module: true, + minimize: process.env.WEBPACK_MODE === PRODUCTION, + // sourceMap: true, + localIdentName: '[local]' + } + }, + { + loader: 'sass-loader', + options: { + implementation: require("sass"), + } + } + ] + }) + }, + { + // ESLint + enforce: 'pre', + test: /\.js$/, + exclude: /node_modules/, + loader: 'eslint-loader', + options: { failOnError: true } + }, + { + // ES6 + test: /\.js$/, + loader: 'babel-loader', + exclude: '/node_modules/' + } + ] + }, + plugins: [ + new ExtractTextPlugin({ + filename: 'sample.[name].css' + }) + ] + }; + + return config; +}; diff --git a/web-basic-sample/.babelrc b/javascript/javascript-basic-syncmanager/.babelrc similarity index 100% rename from web-basic-sample/.babelrc rename to javascript/javascript-basic-syncmanager/.babelrc diff --git a/javascript/javascript-basic-syncmanager/.eslintignore b/javascript/javascript-basic-syncmanager/.eslintignore new file mode 100644 index 00000000..feb309d8 --- /dev/null +++ b/javascript/javascript-basic-syncmanager/.eslintignore @@ -0,0 +1,2 @@ + +**/*.min.js \ No newline at end of file diff --git a/web-basic-sample/.eslintrc.js b/javascript/javascript-basic-syncmanager/.eslintrc.js similarity index 100% rename from web-basic-sample/.eslintrc.js rename to javascript/javascript-basic-syncmanager/.eslintrc.js diff --git a/web-basic-sample/.prettierignore b/javascript/javascript-basic-syncmanager/.prettierignore similarity index 100% rename from web-basic-sample/.prettierignore rename to javascript/javascript-basic-syncmanager/.prettierignore diff --git a/web-basic-sample-syncmanager/.prettierrc b/javascript/javascript-basic-syncmanager/.prettierrc similarity index 100% rename from web-basic-sample-syncmanager/.prettierrc rename to javascript/javascript-basic-syncmanager/.prettierrc diff --git a/javascript/javascript-basic-syncmanager/README.md b/javascript/javascript-basic-syncmanager/README.md new file mode 100644 index 00000000..845d7e55 --- /dev/null +++ b/javascript/javascript-basic-syncmanager/README.md @@ -0,0 +1,87 @@ +# Sendbird SyncManager for JavaScript sample +![Platform](https://img.shields.io/badge/platform-JAVASCRIPT-orange.svg) +![Languages](https://img.shields.io/badge/language-JAVASCRIPT-orange.svg) +[![npm](https://img.shields.io/npm/v/sendbird-syncmanager.svg?style=popout&colorB=red)](https://www.npmjs.com/package/sendbird-syncmanager) + +## Introduction + +SyncManager for Javascript is a Chat SDK add-on that optimizes the user caching experience by interlinking the synchronization of the local data storage with the chat data in Sendbird server through an event-driven structure. Provided here is a SyncManager sample for Javascript to experience first-hand the benefits of Sendbird’s SyncManager. + +### Benefits + +Sendbird SyncManager provides the local caching system and data synchronization with the Sendbird server, which are run on an event-driven structure. According to the real-time events of the messages and channels, SyncManager takes care of the background tasks for the cache updates from the Sendbird server to the local device. By leveraging this systemized structure with connection-based synchronization, SyncManager allows you to easily integrate the Chat SDK to utilize all of its features, while also reducing data usage and offering a reliable and effortless storage mechanism. + +### More about Sendbird SyncManager for JavaScript + +Find out more about Sendbird SyncManager for JavaScript at [SyncManager for JavaScript doc](https://sendbird.com/docs/syncmanager/v1/javascript/getting-started/about-syncmanager). If you need any help in resolving any issues or have questions, visit [our community](https://community.sendbird.com). + +
+ +## Before getting started +This section provides the prerequisites for testing Sendbird Desk for Javascript sample app. + +### Requirements +The minimum requirements for SyncManager for Javascript are: +- Node. js v8+ +- NPM v6+ +- [Chat SDK for JavaScript](https://github.com/sendbird/SendBird-SDK-JavaScript) v3.0 115+ + +### Try the sample app using your data + +If you would like to try the sample app specifically fit to your usage, you can do so by replacing the default sample app ID with yours, which you can obtain by [creating your Sendbird application from the dashboard](https://sendbird.com/docs/chat/v3/javascript/getting-started/install-chat-sdk#2-step-1-create-a-sendbird-application-from-your-dashboard). Furthermore, you could also add data of your choice on the dashboard to test. This will allow you to experience the sample app with data from your Sendbird application. + +### Try the SyncManager on our demo website + +By using this [link](https://sample.sendbird.com/basic/sync-manager), you can test the SyncManager through our demo website. + +
+ +## Getting started + +You can install and run SyncManager for JavaScript sample app on your system using `npm`. + +### Install packages + +`Node` v8.x+ should be installed on your system. + +> `node-sass` package requires XCode developer tools (MacOS only) and Node.js version matching. If you have any trouble in the installation, see https://www.npmjs.com/package/node-sass. + +```bash +npm install +``` + +### Run the sample + +```bash +npm start +``` + +
+ +## Customizing the sample + +To implement customization to the sample, you can use `webpack` for buiding it. + +### Install packages + +`Node` v8.x+ should be installed on your system. + +```bash +npm install +``` + +### Modify files + +If you want to change `APP_ID`, change `APP_ID` in `const.js` to the other `APP_ID` you want. You can test the sample with local server by running the following command. + +```bash +npm run start:dev +``` + +### Build the sample + +When the modification is complete, you'll need to bundle the file using `webpack`. The bundled files are created in the **dist** folder. Please check `webpack.config.js` for settings. + +```bash +npm run build +``` diff --git a/web-basic-sample-syncmanager/chat.html b/javascript/javascript-basic-syncmanager/chat.html similarity index 96% rename from web-basic-sample-syncmanager/chat.html rename to javascript/javascript-basic-syncmanager/chat.html index c71b1ebd..01169d0b 100644 --- a/web-basic-sample-syncmanager/chat.html +++ b/javascript/javascript-basic-syncmanager/chat.html @@ -12,7 +12,7 @@ rel='stylesheet' type='text/css'> - Basic Sample with SyncManager | SendBird + Basic Sample with SyncManager | Sendbird @@ -23,7 +23,7 @@
- SendBird + Sendbird
diff --git a/web-basic-sample-syncmanager/index.html b/javascript/javascript-basic-syncmanager/index.html similarity index 93% rename from web-basic-sample-syncmanager/index.html rename to javascript/javascript-basic-syncmanager/index.html index 41cf8450..e5745b1e 100644 --- a/web-basic-sample-syncmanager/index.html +++ b/javascript/javascript-basic-syncmanager/index.html @@ -15,7 +15,7 @@ - Basic Sample with SyncManager | SendBird + Basic Sample with SyncManager | Sendbird @@ -26,7 +26,7 @@
- SendBird + Sendbird
Web Basic Sample with SyncManager @@ -36,7 +36,7 @@