diff --git a/app/search/client/search/search.js b/app/search/client/search/search.js
index a71cc3270ec19..49750e7a9992d 100644
--- a/app/search/client/search/search.js
+++ b/app/search/client/search/search.js
@@ -7,6 +7,8 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import _ from 'underscore';
+import { isMobile } from '../../../utils/client';
+
Template.RocketSearch.onCreated(function() {
this.provider = new ReactiveVar();
this.isActive = new ReactiveVar(false);
@@ -78,6 +80,9 @@ Template.RocketSearch.onCreated(function() {
});
Template.RocketSearch.events = {
+ 'click .js-close-search'() {
+ Session.set('openSearchPage', !Session.get('openSearchPage'));
+ },
'keydown #message-search'(evt, t) {
if (evt.keyCode === 13) {
if (t.suggestionActive.get() !== undefined) {
@@ -144,6 +149,9 @@ Template.RocketSearch.events = {
};
Template.RocketSearch.helpers({
+ isMobile() {
+ return isMobile();
+ },
error() {
return Template.instance().error.get();
},
diff --git a/app/search/client/style/style.css b/app/search/client/style/style.css
index 87dadc8c355f6..98b6bac816691 100644
--- a/app/search/client/style/style.css
+++ b/app/search/client/style/style.css
@@ -12,6 +12,13 @@
flex: 1;
}
+.back-button {
+ position: relative;
+ top: 36px;
+
+ padding-left: 24px;
+}
+
.rocket-default-search-results {
overflow: auto;
overflow-x: hidden;
diff --git a/app/settings/client/index.js b/app/settings/client/index.js
new file mode 100644
index 0000000000000..481b58fd8a8a2
--- /dev/null
+++ b/app/settings/client/index.js
@@ -0,0 +1,5 @@
+import { settings } from './lib/settings';
+
+export {
+ settings,
+};
diff --git a/app/settings/server/index.js b/app/settings/server/index.js
new file mode 100644
index 0000000000000..3adfad5409b3b
--- /dev/null
+++ b/app/settings/server/index.js
@@ -0,0 +1,6 @@
+import { settings, SettingsEvents } from './functions/settings';
+
+export {
+ settings,
+ SettingsEvents,
+};
diff --git a/app/sms/server/services/twilio.js b/app/sms/server/services/twilio.js
index bf52dcd5491f8..bc4854c035d4d 100644
--- a/app/sms/server/services/twilio.js
+++ b/app/sms/server/services/twilio.js
@@ -79,6 +79,7 @@ class Twilio {
let mediaUrl;
const defaultLanguage = settings.get('Language') || 'en';
+
if (extraData && extraData.fileUpload) {
const { rid, userId, fileUpload: { size, type, publicFilePath } } = extraData;
const user = userId ? Meteor.users.findOne(userId) : null;
@@ -103,6 +104,13 @@ class Twilio {
mediaUrl = [publicFilePath];
}
+ if (extraData && extraData.mediaUrl) {
+ if (mediaUrl) {
+ mediaUrl.push(extraData.mediaUrl);
+ } else {
+ mediaUrl = extraData.mediaUrl;
+ }
+ }
let persistentAction;
if (extraData && extraData.location) {
diff --git a/app/sms/server/settings.js b/app/sms/server/settings.js
index 4a5ffae29bf99..4af8a4d283a5e 100644
--- a/app/sms/server/settings.js
+++ b/app/sms/server/settings.js
@@ -166,5 +166,31 @@ Meteor.startup(function() {
i18nDescription: 'Mobex_sms_gateway_from_numbers_list_desc',
});
});
+
+ this.section('Invitation', function() {
+ this.add('Invitation_SMS_Twilio_From', '', {
+ type: 'string',
+ i18nLabel: 'Invitation_SMS_Twilio_From',
+ });
+ this.add('Invitation_SMS_Customized', false, {
+ type: 'boolean',
+ i18nLabel: 'Custom_SMS',
+ });
+ return this.add('Invitation_SMS_Customized_Body', '', {
+ type: 'code',
+ code: 'text',
+ multiline: true,
+ i18nLabel: 'Body',
+ i18nDescription: 'Invitation_SMS_Customized_Body',
+ enableQuery: {
+ _id: 'Invitation_SMS_Customized',
+ value: true,
+ },
+ i18nDefaultQuery: {
+ _id: 'Invitation_SMS_Default_Body',
+ value: false,
+ },
+ });
+ });
});
});
diff --git a/app/theme/client/imports/components/contextual-bar.css b/app/theme/client/imports/components/contextual-bar.css
index e27c96fefae2d..eb1d31edbb711 100644
--- a/app/theme/client/imports/components/contextual-bar.css
+++ b/app/theme/client/imports/components/contextual-bar.css
@@ -187,13 +187,15 @@
@media (width <= 500px) {
.contextual-bar {
- position: fixed;
- z-index: 999;
- top: 0;
+ &.contextual-bar {
+ position: fixed;
+ z-index: 999;
+ top: 0;
- width: 100%;
+ width: 100%;
- animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
+ animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
+ }
}
}
diff --git a/app/theme/client/imports/components/header.css b/app/theme/client/imports/components/header.css
index ebf39c94879a8..00f42098ede85 100644
--- a/app/theme/client/imports/components/header.css
+++ b/app/theme/client/imports/components/header.css
@@ -20,7 +20,7 @@
&__block {
display: flex;
- margin: 0 -0.5rem;
+ margin: 0 0.5rem;
padding: 0 0.5rem;
@@ -167,12 +167,27 @@
@media (width <= 500px) {
.rc-header {
&__block {
- margin: 0 0.25rem;
+ margin: 0 0.2em;
+
+ padding: 0 0.1em;
}
- &__block-action {
- order: 2;
+ &__section {
+ display: flex;
+
+ margin: 0 0.5rem;
+
+ padding: 0 0.5rem;
+
+ align-items: center;
+ }
+
+ &__first-icon {
+ width: 2.25em;
+ padding: 0;
+ }
+ &__block-action {
& + & {
border-left: 1px var(--color-gray) solid;
@@ -181,6 +196,16 @@
border-left: 0;
}
}
+
+ .rc-room-actions {
+ &__action {
+ .rc-room-actions__button {
+ .tab-button-icon {
+ height: 1.2em;
+ }
+ }
+ }
+ }
}
&__data {
@@ -193,15 +218,42 @@
display: flex;
margin: 0;
+
padding: 0;
}
}
+
+ .rc-channel {
+ &--room {
+ background: var(--veranda-color-primary);
+ }
+
+ &__wrap {
+ color: var(--color-white);
+ background: var(--veranda-color-primary);
+ }
+
+ &__name {
+ color: var(--color-white);
+
+ font-size: 1.3rem;
+
+ & > .rc-header__icon {
+ display: none;
+ }
+ }
+
+ .burger i {
+ background-color: var(--color-white);
+ }
+ }
}
.burger {
position: relative;
cursor: pointer;
+
transition: transform 0.2s ease-out 0.1s;
will-change: transform;
diff --git a/app/theme/client/imports/components/main-content.css b/app/theme/client/imports/components/main-content.css
index 04f7cb05d9706..4e9238db7f483 100644
--- a/app/theme/client/imports/components/main-content.css
+++ b/app/theme/client/imports/components/main-content.css
@@ -2,7 +2,7 @@
position: relative;
- z-index: 0;
+ z-index: 2;
display: flex;
flex-direction: column;
@@ -10,7 +10,8 @@
width: 1vw;
- height: 100%;
+ transition: transform 0.3s;
+ transform: translate3d(100%, 0, 0);
}
.messages-container .room-icon {
diff --git a/app/theme/client/imports/components/pushMessage.css b/app/theme/client/imports/components/pushMessage.css
new file mode 100644
index 0000000000000..04db4fdd67b20
--- /dev/null
+++ b/app/theme/client/imports/components/pushMessage.css
@@ -0,0 +1,148 @@
+.push-message-container {
+ overflow: hidden;
+
+ min-height: 40px;
+ margin-top: 4px;
+ padding: 8px;
+
+ border: 1px solid;
+ border-color: grey;
+ border-radius: 10px;
+
+ line-height: 20px;
+}
+
+.push-message {
+ display: flex;
+
+ &-content-container {
+ margin: 0 8px;
+ }
+
+ &-icon-container {
+ float: left;
+
+ width: 36px;
+ height: 36px;
+
+ margin: 4px 8px 0 0;
+ }
+
+ &-icon {
+ width: 100%;
+ height: 100%;
+
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: cover;
+ }
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &-title {
+ overflow: hidden;
+
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+
+ &-timestamp-container {
+ display: flex;
+ }
+
+ &-timestamp {
+ overflow: hidden;
+
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ font-size: 13px;
+ flex-shrink: 0;
+ }
+
+ &-body {
+ overflow: hidden;
+
+ text-overflow: ellipsis;
+
+ font-size: 14px;
+ }
+
+ &-actions-container {
+ display: flex;
+ overflow: hidden;
+ }
+}
+
+.button-expand {
+ transition: transform 0.2s;
+ transform: rotate(180deg);
+}
+
+.button-collapse {
+ transition: transform 0.2s;
+ transform: rotate(0deg);
+}
+
+.body-collapsed {
+ display: block;
+
+ max-height: 20px;
+
+ white-space: nowrap;
+}
+
+.push-message-action-button {
+ display: flex;
+ overflow: hidden;
+
+ min-height: 40px;
+
+ margin: 4px;
+ padding: 2px 12px;
+
+ border: 1px solid;
+ border-color: grey;
+ border-radius: 10px;
+ align-items: center;
+
+ &:hover {
+ background-color: #dddddd;
+ }
+
+ &-icon-container {
+ float: left;
+ flex: none;
+
+ width: 16px;
+ height: 16px;
+
+ margin-right: 8px;
+ }
+
+ &-icon {
+ width: 100%;
+ height: 100%;
+
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: cover;
+ }
+
+ &-title {
+ overflow: hidden;
+
+ white-space: nowrap;
+
+ text-overflow: ellipsis;
+
+ font-size: 15px;
+ font-weight: bold;
+ }
+}
diff --git a/app/theme/client/imports/components/read-receipts.css b/app/theme/client/imports/components/read-receipts.css
index da73d930416bd..c88dc7ba97cdc 100644
--- a/app/theme/client/imports/components/read-receipts.css
+++ b/app/theme/client/imports/components/read-receipts.css
@@ -9,6 +9,11 @@
height: 0.8em;
}
+/* Overwriting the color-component-color class defined in colors.less */
+.color-component-color {
+ color: grey;
+}
+
.message:hover .read-receipt,
.message.active .read-receipt {
display: none;
diff --git a/app/theme/client/imports/components/share.css b/app/theme/client/imports/components/share.css
new file mode 100644
index 0000000000000..8b1ac94d679ff
--- /dev/null
+++ b/app/theme/client/imports/components/share.css
@@ -0,0 +1,56 @@
+.share-header-container {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.share-icon {
+ display: inline-block;
+
+ box-sizing: border-box;
+
+ width: 25%;
+ min-width: 46px;
+
+ padding: 16px 0;
+
+ cursor: pointer;
+
+ text-align: center;
+
+ border: none;
+
+ background-color: transparent;
+
+ box-shadow: inset 0 0 20px rgba(0, 0, 0, 0);
+
+ font-size: 12px;
+ font-weight: 400;
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ &:hover {
+ box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.125);
+ }
+}
+
+.share-svg-icon {
+ display: block;
+
+ width: 42px;
+ height: 36px;
+ margin: auto;
+
+ &__header {
+ width: 24px;
+ height: 24px;
+ }
+}
+
+.share-icon-title {
+ display: block;
+
+ padding-top: 10px;
+
+ font-size: 14px;
+}
diff --git a/app/theme/client/imports/components/sidebar/rooms-list.css b/app/theme/client/imports/components/sidebar/rooms-list.css
index e952e991a5199..65532010758f0 100644
--- a/app/theme/client/imports/components/sidebar/rooms-list.css
+++ b/app/theme/client/imports/components/sidebar/rooms-list.css
@@ -59,6 +59,20 @@
}
}
+@media (width <= 500px) {
+ .rooms-list {
+ &__type {
+ padding: 0 var(--sidebar-small-default-padding) 1rem var(--sidebar-small-default-padding);
+
+ font-size: var(--rooms-list-title-text-size-mobile);
+ }
+
+ &__empty-room {
+ font-size: var(--rooms-list-empty-text-size-mobile);
+ }
+ }
+}
+
@media (width <= 400px) {
padding: 0 calc(var(--sidebar-small-default-padding) - 4px);
diff --git a/app/theme/client/imports/components/sidebar/sidebar.css b/app/theme/client/imports/components/sidebar/sidebar.css
index 1b8f4e312f032..1ff3e4b2ef7cd 100644
--- a/app/theme/client/imports/components/sidebar/sidebar.css
+++ b/app/theme/client/imports/components/sidebar/sidebar.css
@@ -2,13 +2,14 @@
position: relative;
- z-index: 2;
+ z-index: 0;
display: flex;
flex-direction: column;
flex: 0 0 var(--sidebar-width);
- width: var(--sidebar-width);
+ width: 100%;
+
max-width: var(--sidebar-width);
height: 100%;
@@ -88,7 +89,7 @@
position: absolute;
user-select: none;
- transform: translate3d(-100%, 0, 0);
+ transform: translate3d(0, 0, 0);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
touch-action: pan-y;
-webkit-user-drag: none;
@@ -100,19 +101,20 @@
}
}
-@media (width <= 400px) {
+@media (width <= 500px) {
.sidebar {
flex: 0 0 var(--sidebar-small-width);
- width: var(--sidebar-small-width);
- max-width: var(--sidebar-small-width);
+ width: 100%;
+
+ max-width: none;
&__footer {
display: none;
}
&:not(&--light) {
- transform: translate3d(-100%, 0, 0);
+ transform: translate3d(0, 0, 0);
}
}
diff --git a/app/theme/client/imports/components/table.css b/app/theme/client/imports/components/table.css
index 16da7f6cf3e5e..c817bccc2150b 100644
--- a/app/theme/client/imports/components/table.css
+++ b/app/theme/client/imports/components/table.css
@@ -65,7 +65,7 @@
padding: 0.25rem 0;
- white-space: nowrap;
+ white-space: pre-wrap;
text-overflow: ellipsis;
font-size: 0.9rem;
diff --git a/app/theme/client/imports/forms/button.css b/app/theme/client/imports/forms/button.css
index 463ed17346cad..1165ddfd50842 100644
--- a/app/theme/client/imports/forms/button.css
+++ b/app/theme/client/imports/forms/button.css
@@ -110,6 +110,11 @@
border: 0;
border-color: var(--button-cancel-color);
background-color: var(--button-cancel-color);
+
+ .rc-button-icon {
+ display: none;
+ visibility: hidden;
+ }
}
&--cancel.rc-button--outline {
@@ -216,4 +221,18 @@
.rc-button--full {
width: 100%;
}
+
+ .rc-button--cancel {
+ padding: 0 1rem;
+
+ .rc-button-icon {
+ display: block;
+ visibility: visible;
+ }
+
+ .rc-button-text {
+ display: none;
+ visibility: hidden;
+ }
+ }
}
diff --git a/app/theme/client/imports/general/base.css b/app/theme/client/imports/general/base.css
index 1cf8605b6b6a0..318501c0840ab 100644
--- a/app/theme/client/imports/general/base.css
+++ b/app/theme/client/imports/general/base.css
@@ -38,6 +38,8 @@ body {
@media (width <= 500px) {
body {
position: fixed;
+
+ font-size: 1rem;
}
}
diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css
index cd8294c202cfc..7a8068481a6fc 100644
--- a/app/theme/client/imports/general/base_old.css
+++ b/app/theme/client/imports/general/base_old.css
@@ -308,7 +308,7 @@
& > div {
- width: 60%;
+ width: 55%;
min-height: 2.5rem;
& label {
@@ -2225,6 +2225,7 @@
& .body {
display: inline;
+ font-size: 12px;
font-style: italic;
& em {
diff --git a/app/theme/client/imports/general/forms.css b/app/theme/client/imports/general/forms.css
index 9516f91a2072f..ba8168c24dc42 100644
--- a/app/theme/client/imports/general/forms.css
+++ b/app/theme/client/imports/general/forms.css
@@ -198,15 +198,13 @@
}
&__content {
-
display: flex;
overflow-y: auto;
flex-direction: column;
-
flex: 1;
- width: 100%;
- padding: 25px 0;
+ margin: 0 -1.25rem;
+ padding: 1.25rem 1.25rem 0;
}
&--apps .preferences-page__header {
diff --git a/app/theme/client/imports/general/reset.css b/app/theme/client/imports/general/reset.css
index b6ed2ac1ba86f..b652ff39824ab 100644
--- a/app/theme/client/imports/general/reset.css
+++ b/app/theme/client/imports/general/reset.css
@@ -100,6 +100,92 @@ video {
}
}
+@media (max-width: 400px) {
+ html,
+ body,
+ div,
+ span,
+ applet,
+ object,
+ iframe,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ p,
+ blockquote,
+ pre,
+ a,
+ abbr,
+ acronym,
+ address,
+ big,
+ cite,
+ code,
+ del,
+ dfn,
+ em,
+ img,
+ ins,
+ kbd,
+ q,
+ s,
+ samp,
+ small,
+ strike,
+ strong,
+ sub,
+ sup,
+ tt,
+ var,
+ b,
+ u,
+ i,
+ center,
+ dl,
+ dt,
+ dd,
+ ol,
+ ul,
+ li,
+ fieldset,
+ form,
+ label,
+ legend,
+ table,
+ caption,
+ tbody,
+ tfoot,
+ thead,
+ tr,
+ th,
+ td,
+ article,
+ aside,
+ canvas,
+ details,
+ embed,
+ figure,
+ figcaption,
+ footer,
+ header,
+ hgroup,
+ menu,
+ nav,
+ output,
+ ruby,
+ section,
+ summary,
+ time,
+ mark,
+ audio,
+ video {
+ display: auto;
+ }
+}
+
/* HTML5 display-role reset for older browsers */
article,
diff --git a/app/theme/client/imports/general/variables.css b/app/theme/client/imports/general/variables.css
index 635786e6a0ccf..b497845d3e495 100644
--- a/app/theme/client/imports/general/variables.css
+++ b/app/theme/client/imports/general/variables.css
@@ -65,6 +65,7 @@
--rc-color-announcement-background: #d1ebfe;
--rc-color-announcement-text-hover: #01336b;
--rc-color-announcement-background-hover: #76b7fc;
+ --veranda-color-primary: #008085;
/* #endregion */
@@ -236,6 +237,7 @@
* Sidebar Account
*/
--sidebar-account-thumb-size: 23px;
+ --sidebar-account-thumb-size-mobile: 1.75em;
--sidebar-small-account-thumb-size: 40px;
--sidebar-account-status-bullet-size: 10px;
--sidebar-small-account-status-bullet-size: 8px;
@@ -269,6 +271,7 @@
--sidebar-item-user-status-size: 6px;
--sidebar-item-user-status-radius: 50%;
--sidebar-item-text-size: 0.875rem;
+ --sidebar-item-text-size-mobile: 1rem;
/*
* Modal - Create Channel
@@ -289,8 +292,10 @@
*/
--rooms-list-title-color: var(--rc-color-primary-light);
--rooms-list-title-text-size: 0.75rem;
+ --rooms-list-title-text-size-mobile: 1rem;
--rooms-list-empty-text-color: var(--color-gray);
--rooms-list-empty-text-size: 0.75rem;
+ --rooms-list-empty-text-size-mobile: 1rem;
--rooms-list-padding: var(--sidebar-default-padding);
--rooms-list-small-padding: var(--sidebar-small-default-padding);
diff --git a/app/theme/client/main.css b/app/theme/client/main.css
index 1795e14bbef84..01bf0f22680be 100644
--- a/app/theme/client/main.css
+++ b/app/theme/client/main.css
@@ -42,6 +42,8 @@
@import 'imports/components/emojiPicker.css';
@import 'imports/components/table.css';
@import 'imports/components/tabs.css';
+@import 'imports/components/share.css';
+@import 'imports/components/pushMessage.css';
/* Modal */
@import 'imports/components/modal/create-channel.css';
diff --git a/app/ui-login/client/login/form.js b/app/ui-login/client/login/form.js
index 198d6c49d3557..c59d051366e88 100644
--- a/app/ui-login/client/login/form.js
+++ b/app/ui-login/client/login/form.js
@@ -126,6 +126,7 @@ Template.loginForm.events({
} if (error && error.error === 'error-user-is-not-activated') {
return instance.state.set('wait-activation');
}
+ callbacks.run('onUserLogin');
Session.set('forceLogin', false);
});
});
@@ -157,6 +158,7 @@ Template.loginForm.events({
return toastr.error(t('User_not_found_or_incorrect_password'));
}
}
+ callbacks.run('onUserLogin');
Session.set('forceLogin', false);
});
}
diff --git a/app/ui-login/client/login/services.js b/app/ui-login/client/login/services.js
index 2b8a04f1cc6c6..6863e5d1f06f4 100644
--- a/app/ui-login/client/login/services.js
+++ b/app/ui-login/client/login/services.js
@@ -5,6 +5,7 @@ import { ServiceConfiguration } from 'meteor/service-configuration';
import toastr from 'toastr';
import { CustomOAuth } from '../../../custom-oauth';
+import { callbacks } from '../../../callbacks';
Meteor.startup(function() {
return ServiceConfiguration.configurations.find({
@@ -88,6 +89,8 @@ Template.loginServices.events({
} else {
toastr.error(error.message);
}
+ } else {
+ callbacks.run('onUserLogin');
}
});
},
diff --git a/app/ui-master/client/main.js b/app/ui-master/client/main.js
index fd855764af5f0..2cf6317b5e331 100644
--- a/app/ui-master/client/main.js
+++ b/app/ui-master/client/main.js
@@ -11,7 +11,7 @@ import { CachedChatSubscription, Roles, Users } from '../../models';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { tooltip } from '../../ui/client/components/tooltip';
import { callbacks } from '../../callbacks/client';
-import { isSyncReady } from '../../../client/lib/userData';
+// import { isSyncReady } from '../../../client/lib/userData';
import { fireGlobalEvent } from '../../ui-utils/client';
import './main.html';
@@ -41,9 +41,12 @@ Template.main.helpers({
return iframeEnabled && iframeLogin.reactiveIframeUrl.get();
},
subsReady: () => {
+ const userReady = Meteor.user();
const subscriptionsReady = CachedChatSubscription.ready.get();
+
const settingsReady = settings.cachedCollection.ready.get();
- const ready = !Meteor.userId() || (isSyncReady.get() && subscriptionsReady && settingsReady);
+
+ const ready = (userReady && subscriptionsReady && settingsReady) || !Meteor.userId();
CachedCollectionManager.syncEnabled = ready;
mainReady.set(ready);
diff --git a/app/ui-master/server/index.js b/app/ui-master/server/index.js
index ec4dc5fd17004..766a7c8a6dae2 100644
--- a/app/ui-master/server/index.js
+++ b/app/ui-master/server/index.js
@@ -49,7 +49,14 @@ Meteor.startup(() => {
}
});
- settings.get('theme-color-sidebar-background', (key, value) => {
+ // settings.get('theme-color-sidebar-background', (key, value) => {
+ // const escapedValue = escapeHTML(value);
+ // injectIntoHead(key, `
`);
+ // });
+
+ // WIDE CHAT
+ settings.get('theme-color-rc-color-primary', (key, value) => {
const escapedValue = escapeHTML(value);
injectIntoHead(key, `
`);
diff --git a/app/ui-message/client/index.js b/app/ui-message/client/index.js
index ab52f2f87da5c..ae5ee7ade836d 100644
--- a/app/ui-message/client/index.js
+++ b/app/ui-message/client/index.js
@@ -6,3 +6,5 @@ import './popup/messagePopupChannel';
import './popup/messagePopupConfig';
import './popup/messagePopupEmoji';
import './popup/messagePopupSlashCommandPreview';
+import './pushMessage';
+import './pushMessageAction';
diff --git a/app/ui-message/client/message.html b/app/ui-message/client/message.html
index 5e3d05079d970..9bb8a6aec82bb 100644
--- a/app/ui-message/client/message.html
+++ b/app/ui-message/client/message.html
@@ -126,6 +126,14 @@
{{> reactAttachments attachments=msg.attachments file=msg.file }}
{{/if}}
+ {{#if isPushMessage}}
+ {{> pushMessage msg=msg}}
+ {{/if}}
+
+ {{#if msg.drid}}
+ {{> DiscussionMetric count=msg.dcount drid=msg.drid lm=msg.dlm openDiscussion=actions.openDiscussion }}
+ {{/if}}
+
{{#with readReceipt}}
diff --git a/app/ui-message/client/message.js b/app/ui-message/client/message.js
index ecf101d212105..7196358286996 100644
--- a/app/ui-message/client/message.js
+++ b/app/ui-message/client/message.js
@@ -446,6 +446,10 @@ Template.message.helpers({
const { msg: { threadMsg } } = this;
return threadMsg;
},
+ isPushMessage() {
+ const { msg } = this;
+ return msg.pushm;
+ },
showStar() {
const { msg } = this;
return msg.starred && msg.starred.length > 0 && msg.starred.find((star) => star._id === Meteor.userId()) && !(msg.actionContext === 'starred' || this.context === 'starred');
diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js
index 7fcf12f371a90..7bf08b4ac3060 100644
--- a/app/ui-message/client/messageBox/messageBox.js
+++ b/app/ui-message/client/messageBox/messageBox.js
@@ -82,6 +82,7 @@ Template.messageBox.onCreated(function() {
autogrow.update();
};
+ // let isSending = false;
this.send = (event) => {
const { input } = this;
diff --git a/app/ui-message/client/messageBox/messageBoxActions.js b/app/ui-message/client/messageBox/messageBoxActions.js
index e75406b062d78..f43ced7cb6881 100644
--- a/app/ui-message/client/messageBox/messageBoxActions.js
+++ b/app/ui-message/client/messageBox/messageBoxActions.js
@@ -26,7 +26,7 @@ messageBox.actions.add('Create_new', 'Video_message', {
messageBox.actions.add('Add_files_from', 'Computer', {
id: 'file-upload',
- icon: 'computer',
+ icon: 'clip',
condition: () => settings.get('FileUpload_Enabled'),
action({ rid, tmid, event, messageBox }) {
event.preventDefault();
diff --git a/app/ui-message/client/popup/messagePopup.js b/app/ui-message/client/popup/messagePopup.js
index 3b9ed5eac4c98..6a064ad1077bd 100644
--- a/app/ui-message/client/popup/messagePopup.js
+++ b/app/ui-message/client/popup/messagePopup.js
@@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
+import { isMobile } from '../../../utils/client';
import './messagePopup.html';
const keys = {
@@ -18,6 +19,10 @@ const keys = {
ARROW_DOWN: 40,
};
+let touchMoved = false;
+let lastTouchX = null;
+let lastTouchY = null;
+
function getCursorPosition(input) {
if (input == null) {
return;
@@ -78,6 +83,9 @@ Template.messagePopup.onCreated(function() {
return _id;
});
template.up = () => {
+ if (isMobile()) {
+ return;
+ }
const current = template.find('.popup-item.selected');
const previous = $(current).prev('.popup-item')[0] || template.find('.popup-item:last-child');
if (previous != null) {
@@ -88,6 +96,9 @@ Template.messagePopup.onCreated(function() {
}
};
template.down = () => {
+ if (isMobile()) {
+ return;
+ }
const current = template.find('.popup-item.selected');
const next = $(current).next('.popup-item')[0] || template.find('.popup-item');
if (next && next.classList.contains('popup-item')) {
@@ -98,7 +109,7 @@ Template.messagePopup.onCreated(function() {
}
};
template.verifySelection = () => {
- if (!template.open.curValue) {
+ if (!template.open.curValue || isMobile()) {
return;
}
const current = template.find('.popup-item.selected');
@@ -235,7 +246,7 @@ Template.messagePopup.onRendered(function() {
if (this.data.getInput != null) {
this.input = typeof this.data.getInput === 'function' && this.data.getInput();
} else if (this.data.input) {
- this.input = this.parentTemplate().find(this.data.input);
+ this.input = this.parentTemplate(this.data.parent).find(this.data.input);
}
if (this.input == null) {
console.error('Input not found for popup');
@@ -271,7 +282,7 @@ Template.messagePopup.onDestroyed(function() {
Template.messagePopup.events({
'mouseenter .popup-item'(e) {
- if (e.currentTarget.className.indexOf('selected') > -1) {
+ if (e.currentTarget.className.indexOf('selected') > -1 || isMobile()) {
return;
}
const template = Template.instance();
@@ -282,12 +293,43 @@ Template.messagePopup.events({
e.currentTarget.className += ' selected sidebar-item__popup-active';
return template.value.set(this._id);
},
- 'mousedown .popup-item, touchstart .popup-item'() {
+ 'mousedown .popup-item'() {
const template = Template.instance();
template.clickingItem = true;
},
- 'mouseup .popup-item, touchend .popup-item'(e) {
- e.stopPropagation();
+ 'touchstart .popup-item'(e) {
+ const { touches } = e.originalEvent;
+ if (touches && touches.length) {
+ lastTouchX = touches[0].pageX;
+ lastTouchY = touches[0].pageY;
+ }
+ touchMoved = false;
+ },
+ 'touchmove .popup-item'(e) {
+ const { touches } = e.originalEvent;
+ if (touches && touches.length) {
+ const deltaX = Math.abs(lastTouchX - touches[0].pageX);
+ const deltaY = Math.abs(lastTouchY - touches[0].pageY);
+ if (deltaX > 5 || deltaY > 5) {
+ touchMoved = true;
+ }
+ }
+ },
+ 'touchend .popup-item'(e) {
+ const template = Template.instance();
+ if (!touchMoved) {
+ template.value.set(this._id);
+ template.enterValue();
+ template.open.set(false);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+ 'mouseup .popup-item'(e) {
+ // To prevent refreshing of page in Mobile client.
+ if (isMobile()) {
+ return;
+ }
const template = Template.instance();
const wasMenuIconClicked = e.target.classList.contains('sidebar-item__menu-icon');
template.clickingItem = false;
diff --git a/app/ui-message/client/pushMessage.html b/app/ui-message/client/pushMessage.html
new file mode 100644
index 0000000000000..b4de733672a4f
--- /dev/null
+++ b/app/ui-message/client/pushMessage.html
@@ -0,0 +1,46 @@
+
+ {{#with data}}
+
+
+ {{#if icon}}
+
+
+
+ {{/if}}
+
+
+ {{#if body}}
+
+ {{body}}
+
+ {{/if}}
+
+
+ {{#if actions}}
+
+ {{#each action in actions}}
+ {{> pushMessageAction data=action}}
+ {{/each}}
+
+ {{/if}}
+
+ {{/with}}
+
diff --git a/app/ui-message/client/pushMessage.js b/app/ui-message/client/pushMessage.js
new file mode 100644
index 0000000000000..3d8b6c39665a1
--- /dev/null
+++ b/app/ui-message/client/pushMessage.js
@@ -0,0 +1,46 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+
+import { timeAgo } from '../../lib/client/lib/formatDate';
+import './pushMessage.html';
+
+Template.pushMessage.helpers({
+ data() {
+ const { _id, pushm_post_processed, pushm_scope, pushm_origin, msg, post_processed_message } = this.msg;
+
+ if (!pushm_post_processed) {
+ navigator.serviceWorker.ready.then((serviceWorkerRegistration) => {
+ console.log('Pushing message to service worker for post processing');
+ const promise = serviceWorkerRegistration.monitorNotification(pushm_origin);
+ serviceWorkerRegistration.pushManager.dispatchMessage(pushm_scope, msg);
+ promise.then((post_processed_message) => {
+ const newMsg = {};
+ console.log(post_processed_message);
+ newMsg.title = post_processed_message.title;
+ newMsg.body = post_processed_message.body;
+ newMsg.icon = post_processed_message.icon;
+ newMsg.actions = post_processed_message.actions;
+ newMsg.timestamp = post_processed_message.timestamp;
+ Meteor.call('savePostProcessedMessage', _id, newMsg);
+ });
+ });
+ } else {
+ return post_processed_message;
+ }
+ },
+ timeAgo(date) {
+ return timeAgo(date);
+ },
+});
+
+Template.pushMessage.events({
+ 'click .button-collapse': (e) => {
+ $(e.delegateTarget).find('.button-down').removeClass('button-collapse').addClass('button-expand');
+ $(e.delegateTarget).find('.push-message-body').removeClass('body-collapsed');
+ },
+
+ 'click .button-expand': (e) => {
+ $(e.delegateTarget).find('.button-down').removeClass('button-expand').addClass('button-collapse');
+ $(e.delegateTarget).find('.push-message-body').addClass('body-collapsed');
+ },
+});
diff --git a/app/ui-message/client/pushMessageAction.html b/app/ui-message/client/pushMessageAction.html
new file mode 100644
index 0000000000000..ad4a49a5fec19
--- /dev/null
+++ b/app/ui-message/client/pushMessageAction.html
@@ -0,0 +1,17 @@
+
+ {{#with data}}
+
+ {{/with}}
+
diff --git a/app/ui-message/client/pushMessageAction.js b/app/ui-message/client/pushMessageAction.js
new file mode 100644
index 0000000000000..13b90d17a7a7c
--- /dev/null
+++ b/app/ui-message/client/pushMessageAction.js
@@ -0,0 +1,14 @@
+import { Template } from 'meteor/templating';
+
+import './pushMessageAction.html';
+
+Template.pushMessage.helpers({
+});
+
+Template.pushMessage.events({
+ 'click .push-message-action-button'(event) {
+ alert(this.action);
+ event.stopPropagation();
+ event.preventDefault();
+ },
+});
diff --git a/app/ui-share/README.md b/app/ui-share/README.md
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/app/ui-share/client/index.js b/app/ui-share/client/index.js
new file mode 100644
index 0000000000000..c9281cc5bff87
--- /dev/null
+++ b/app/ui-share/client/index.js
@@ -0,0 +1,2 @@
+import './share.html';
+import './share';
diff --git a/app/ui-share/client/share.html b/app/ui-share/client/share.html
new file mode 100644
index 0000000000000..482e7fc12c8f9
--- /dev/null
+++ b/app/ui-share/client/share.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/app/ui-share/client/share.js b/app/ui-share/client/share.js
new file mode 100644
index 0000000000000..9a0b358277e20
--- /dev/null
+++ b/app/ui-share/client/share.js
@@ -0,0 +1,71 @@
+import { Template } from 'meteor/templating';
+
+import { getShareData } from '../../utils';
+
+function getShareString() {
+ const data = getShareData();
+ return `${ data.title } \n${ data.url } \n${ data.text }`;
+}
+
+function fallbackCopyTextToClipboard(text) {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed'; // avoid scrolling to bottom
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ document.execCommand('copy');
+ } catch (err) {
+ console.error('Unable to copy', err);
+ }
+
+ document.body.removeChild(textArea);
+}
+
+Template.share.helpers({
+
+});
+
+Template.share.events({
+ 'click [data-type="copy"]'() {
+ if (!navigator.clipboard) {
+ fallbackCopyTextToClipboard(getShareString());
+ return;
+ }
+ navigator.clipboard.writeText(getShareString());
+ },
+ 'click [data-type="print"]'() {
+ self.print();
+ },
+ 'click [data-type="email"]'() {
+ const { title } = getShareData();
+ window.open(`mailto:?subject=${ title }&body=${ getShareString() }`);
+ },
+ 'click [data-type="sms"]'() {
+ location.href = `sms:?&body=${ getShareString() }`;
+ },
+
+
+ 'click [data-type="facebook"]'() {
+ const { url } = getShareData();
+ window.open(`https://www.facebook.com/sharer/sharer.php?u=${ encodeURIComponent(url) }`);
+ },
+ 'click [data-type="whatsapp"]'() {
+ window.open(`https://api.whatsapp.com/send?text=${ encodeURIComponent(getShareString()) }`);
+ },
+ 'click [data-type="twitter"]'() {
+ const { url } = getShareData();
+ window.open(`http://twitter.com/share?text=${ getShareString() }&url=${ url }`);
+ },
+ 'click [data-type="linkedin"]'() {
+ const { title, url } = getShareData();
+ window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${ url }&title=${ title }&summary=${ getShareString() }&source=LinkedIn`);
+ },
+ 'click [data-type="telegram"]'() {
+ const { url } = getShareData();
+ window.open(`https://telegram.me/share/msg?url=${ url }&text=${ getShareString() }`);
+ },
+
+});
diff --git a/app/ui-share/index.js b/app/ui-share/index.js
new file mode 100644
index 0000000000000..40a7340d38877
--- /dev/null
+++ b/app/ui-share/index.js
@@ -0,0 +1 @@
+export * from './client/index';
diff --git a/app/ui-sidenav/client/roomList.js b/app/ui-sidenav/client/roomList.js
index eaa21ad59d231..2823c2500906f 100644
--- a/app/ui-sidenav/client/roomList.js
+++ b/app/ui-sidenav/client/roomList.js
@@ -325,7 +325,7 @@ const mergeRoomSub = (room) => {
Subscriptions.update({
rid: room._id,
- lm: { $lt: room.lm },
+ lm: { $lt: (room.lastMessage && room.lastMessage.ts) || room.lm },
}, {
$set: {
lm: room.lm,
diff --git a/app/ui-sidenav/client/sideNav.js b/app/ui-sidenav/client/sideNav.js
index 50b75cc8adb8c..fddca9968693f 100644
--- a/app/ui-sidenav/client/sideNav.js
+++ b/app/ui-sidenav/client/sideNav.js
@@ -6,7 +6,7 @@ import { Template } from 'meteor/templating';
import { SideNav, menu } from '../../ui-utils';
import { settings } from '../../settings';
-import { roomTypes, getUserPreference } from '../../utils';
+import { roomTypes, getUserPreference, isMobile } from '../../utils';
import { Users } from '../../models';
Template.sideNav.helpers({
@@ -35,8 +35,11 @@ Template.sideNav.helpers({
},
sidebarViewMode() {
+ if (isMobile()) {
+ return 'extended';
+ }
const viewMode = getUserPreference(Meteor.userId(), 'sidebarViewMode');
- return viewMode || 'condensed';
+ return viewMode || 'extended';
},
sidebarHideAvatar() {
@@ -100,10 +103,25 @@ const redirectToDefaultChannelIfNeeded = () => {
});
};
+const openMainContentIfNeeded = () => {
+ const currentRouteState = FlowRouter.current();
+ const defaults = ['/', '/home', '/account'];
+
+ if (defaults.includes(currentRouteState.path)) {
+ menu.open();
+ } else {
+ menu.close();
+ }
+};
+
Template.sideNav.onRendered(function() {
SideNav.init();
menu.init();
redirectToDefaultChannelIfNeeded();
+ Tracker.autorun(function() {
+ FlowRouter.watchPathChange();
+ openMainContentIfNeeded();
+ });
return Meteor.defer(() => menu.updateUnreadBars());
});
diff --git a/app/ui-utils/client/index.js b/app/ui-utils/client/index.js
index ff68ad0c9549f..30102dee7ea4c 100644
--- a/app/ui-utils/client/index.js
+++ b/app/ui-utils/client/index.js
@@ -6,6 +6,7 @@ export { call } from './lib/callMethod';
export { erase, hide, leave } from './lib/ChannelActions';
export { MessageAction } from './lib/MessageAction';
export { messageBox } from './lib/messageBox';
+export { offlineAction } from './lib/offlineAction';
export { popover } from './lib/popover';
export { readMessage } from './lib/readMessages';
export { RoomManager } from './lib/RoomManager';
diff --git a/app/ui-utils/client/lib/RoomHistoryManager.js b/app/ui-utils/client/lib/RoomHistoryManager.js
index 5e6a169b6d8a1..f5bdd066edde6 100644
--- a/app/ui-utils/client/lib/RoomHistoryManager.js
+++ b/app/ui-utils/client/lib/RoomHistoryManager.js
@@ -12,8 +12,8 @@ import { RoomManager } from './RoomManager';
import { readMessage } from './readMessages';
import { renderMessageBody } from '../../../../client/lib/renderMessageBody';
import { getConfig } from '../config';
-import { ChatMessage, ChatSubscription, ChatRoom } from '../../../models';
import { call } from './callMethod';
+import { ChatMessage, ChatSubscription, ChatRoom } from '../../../models';
import { filterMarkdown } from '../../../markdown/lib/markdown';
import { getUserPreference } from '../../../utils/client';
@@ -320,7 +320,6 @@ export const RoomHistoryManager = new class extends Emitter {
const subscription = ChatSubscription.findOne({ rid: message.rid });
if (subscription) {
- // const { ls } = subscription;
typeName = subscription.t + subscription.name;
} else {
const curRoomDoc = ChatRoom.findOne({ _id: message.rid });
@@ -391,7 +390,20 @@ export const RoomHistoryManager = new class extends Emitter {
}
clear(rid) {
- ChatMessage.remove({ rid });
+ const query = { rid };
+ const options = {
+ sort: {
+ ts: -1,
+ },
+ limit: 50,
+ };
+ const retain = ChatMessage.find(query, options).fetch();
+ ChatMessage.remove({
+ rid,
+ ts: {
+ $lt: retain[retain.length - 1].ts,
+ },
+ });
if (this.histories[rid]) {
this.histories[rid].hasMore.set(true);
this.histories[rid].isLoading.set(false);
diff --git a/app/ui-utils/client/lib/RoomManager.js b/app/ui-utils/client/lib/RoomManager.js
index ed01f1766fd5c..e0e41475ffed2 100644
--- a/app/ui-utils/client/lib/RoomManager.js
+++ b/app/ui-utils/client/lib/RoomManager.js
@@ -350,7 +350,7 @@ Meteor.startup(() => {
Tracker.autorun(function() {
if (Meteor.userId()) {
return Notifications.onUser('message', function(msg) {
- msg.u = msg.u || { username: 'rocket.cat' };
+ msg.u = msg.u || { username: 'viasat' };
msg.private = true;
return ChatMessage.upsert({ _id: msg._id }, msg);
diff --git a/app/ui-utils/client/lib/SideNav.js b/app/ui-utils/client/lib/SideNav.js
index fe0b5bab0c102..bc524bcae8c1d 100644
--- a/app/ui-utils/client/lib/SideNav.js
+++ b/app/ui-utils/client/lib/SideNav.js
@@ -5,6 +5,7 @@ import { AccountBox } from './AccountBox';
import { roomTypes } from '../../../utils/client/lib/roomTypes';
import { Subscriptions } from '../../../models';
import { RoomManager } from '../../../../client/lib/RoomManager';
+import { isMobile } from '../../../utils/client/lib/isMobile';
export const SideNav = new class {
constructor() {
@@ -50,7 +51,11 @@ export const SideNav = new class {
if (!routesNamesForRooms.includes(FlowRouter.current().route.name)) {
const subscription = Subscriptions.findOne({ rid: RoomManager.lastRid });
if (subscription) {
- roomTypes.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams);
+ if (isMobile()) {
+ FlowRouter.go('home');
+ } else {
+ roomTypes.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams);
+ }
} else {
FlowRouter.go('home');
}
diff --git a/app/ui-utils/client/lib/menu.js b/app/ui-utils/client/lib/menu.js
index 918c9f19e2d5b..35031d1fb42bf 100644
--- a/app/ui-utils/client/lib/menu.js
+++ b/app/ui-utils/client/lib/menu.js
@@ -1,3 +1,4 @@
+import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
@@ -6,7 +7,6 @@ import { Emitter } from '@rocket.chat/emitter';
import { isRtl } from '../../../utils';
const sideNavW = 280;
-const map = (x, in_min, in_max, out_min, out_max) => (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
export const menu = new class extends Emitter {
constructor() {
@@ -104,23 +104,20 @@ export const menu = new class extends Emitter {
this.diff = 0;
}
}
- // if (map((this.diff / sideNavW), 0, 1, -.1, .8) > 0) {
this.sidebar.css('box-shadow', '0 0 15px 1px rgba(0,0,0,.3)');
- // this.sidebarWrap.css('z-index', '9998');
this.translate(this.diff);
- // }
}
}
- translate(diff, width = sideNavW) {
+ translate(diff) {
if (diff === undefined) {
diff = this.isRtl ? -1 * sideNavW : sideNavW;
}
- this.sidebarWrap.css('width', '100%');
+
this.wrapper.css('overflow', 'hidden');
- this.sidebarWrap.css('background-color', '#000');
- this.sidebarWrap.css('opacity', map(Math.abs(diff) / width, 0, 1, -0.1, 0.8).toFixed(2));
- this.isRtl ? this.sidebar.css('transform', `translate3d(${ (sideNavW + diff).toFixed(3) }px, 0 , 0)`) : this.sidebar.css('transform', `translate3d(${ (diff - sideNavW).toFixed(3) }px, 0 , 0)`);
+
+ // WIDECHAT translate main content
+ this.isRtl ? this.mainContent.css('transform', `translate3d(${ diff.toFixed(3) }px, 0 , 0)`) : this.mainContent.css('transform', `translate3d(${ diff.toFixed(3) }px, 0 , 0)`);
}
touchend() {
@@ -154,6 +151,8 @@ export const menu = new class extends Emitter {
this.sidebar = this.menu;
this.sidebarWrap = $('.sidebar-wrap');
this.wrapper = $('.messages-box > .wrapper');
+ this.mainContent = $('.main-content');
+
const ignore = (fn) => (event) => document.body.clientWidth <= 780 && fn(event);
document.body.addEventListener('touchstart', ignore((e) => this.touchstart(e)));
@@ -163,21 +162,21 @@ export const menu = new class extends Emitter {
e.target === this.sidebarWrap[0] && this.isOpen() && this.emit('clickOut', e);
}));
this.on('close', () => {
- this.sidebarWrap.css('width', '');
- // this.sidebarWrap.css('z-index', '');
- this.sidebarWrap.css('background-color', '');
- this.sidebar.css('transform', '');
- this.sidebar.css('box-shadow', '');
- this.sidebar.css('transition', '');
- this.sidebarWrap.css('transition', '');
- this.wrapper && this.wrapper.css('overflow', '');
+ // WIDECHAT open main content
+ this.mainContent.css('transform', 'translate3d( 0, 0 , 0)');
});
this.on('open', ignore(() => {
- this.sidebar.css('box-shadow', '0 0 15px 1px rgba(0,0,0,.3)');
- // this.sidebarWrap.css('z-index', '9998');
- this.translate();
+ // WIDECHAT close main content
+ this.mainContent.css('transform', 'translate3d( 100%, 0 , 0)');
+ if (!FlowRouter.current().path.startsWith('/admin')
+ && !FlowRouter.current().path.startsWith('/account')
+ && !FlowRouter.current().path.startsWith('/omnichannel')) {
+ FlowRouter.withReplaceState(function() {
+ FlowRouter.go('/home');
+ });
+ Session.set('openSearchPage', false);
+ }
}));
- this.mainContent = $('.main-content');
this.list = $('.rooms-list');
this._open = false;
diff --git a/app/ui-utils/client/lib/offlineAction.js b/app/ui-utils/client/lib/offlineAction.js
new file mode 100644
index 0000000000000..a217050deaac3
--- /dev/null
+++ b/app/ui-utils/client/lib/offlineAction.js
@@ -0,0 +1,11 @@
+import { Meteor } from 'meteor/meteor';
+import toastr from 'toastr';
+
+import { t } from '../../../utils';
+
+export const offlineAction = (action) => {
+ if (Meteor.status().status === 'connected') {
+ return false;
+ }
+ return toastr.info(t('Check_your_internet_connection', { action }));
+};
diff --git a/app/ui-utils/client/lib/openRoom.js b/app/ui-utils/client/lib/openRoom.js
index 30a8e7f20cb60..7a82d97b70abd 100644
--- a/app/ui-utils/client/lib/openRoom.js
+++ b/app/ui-utils/client/lib/openRoom.js
@@ -63,7 +63,6 @@ export const openRoom = async function(type, name, render = true) {
render && appLayout.render('main', { center: 'room' });
-
c.stop();
if (window.currentTracker) {
diff --git a/app/ui-utils/client/lib/popover.js b/app/ui-utils/client/lib/popover.js
index 014212e10a7fa..5721d61aca965 100644
--- a/app/ui-utils/client/lib/popover.js
+++ b/app/ui-utils/client/lib/popover.js
@@ -1,8 +1,10 @@
import './popover.html';
import { Blaze } from 'meteor/blaze';
+import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import _ from 'underscore';
+import { share, isShareAvailable } from '../../../utils';
import { messageBox } from './messageBox';
import { MessageAction } from './MessageAction';
import { isRtl } from '../../../utils/client';
@@ -28,6 +30,7 @@ export const popover = {
if (activeElement) {
$(activeElement).removeClass('active');
}
+ this.renderedPopover = null;
},
};
@@ -138,6 +141,17 @@ Template.popover.onRendered(function() {
this.firstNode.style.visibility = 'visible';
});
+// WIDE CHAT
+Template.popover.onCreated(function() {
+ this.route = FlowRouter.current().path;
+ this.autorun(() => {
+ FlowRouter.watchPathChange();
+ if (FlowRouter.current().path !== this.route) {
+ popover.close();
+ }
+ });
+});
+
Template.popover.onDestroyed(function() {
if (this.data.onDestroyed) {
this.data.onDestroyed();
@@ -148,7 +162,7 @@ Template.popover.onDestroyed(function() {
Template.popover.events({
'click .js-action'(e, instance) {
!this.action || this.action.call(this, e, instance.data.data);
- popover.close();
+ !this.hasPopup && popover.close();
},
'click .js-close'() {
popover.close();
@@ -177,6 +191,23 @@ Template.popover.events({
return false;
}
},
+ 'click [data-type="share-action"]'(e) {
+ if (isShareAvailable()) {
+ share();
+ } else {
+ popover.close();
+ const options = [];
+ const config = {
+ template: 'share',
+ currentTarget: e.target,
+ data: {
+ options,
+ },
+ offsetVertical: e.target.clientHeight + 10,
+ };
+ popover.open(config);
+ }
+ },
});
Template.popover.helpers({
diff --git a/app/ui/client/components/header/header.html b/app/ui/client/components/header/header.html
index 14fb89cd3939f..31350fce6786b 100644
--- a/app/ui/client/components/header/header.html
+++ b/app/ui/client/components/header/header.html
@@ -8,7 +8,7 @@
{{#if rawSectionName}}
{{else}}
-
+
{{/if}}
{{#if Template.contentBlock}}
diff --git a/app/ui/client/index.js b/app/ui/client/index.js
index e32c0ec70a783..93d84c46e7b36 100644
--- a/app/ui/client/index.js
+++ b/app/ui/client/index.js
@@ -50,6 +50,7 @@ export { fileUpload } from './lib/fileUpload';
export { MsgTyping } from './lib/msgTyping';
export { KonchatNotification } from './lib/notification';
export { Login, Button } from './lib/rocket';
+export { sendOfflineFileMessage } from './lib/sendOfflineFileMessage';
export { AudioRecorder } from './lib/recorderjs/audioRecorder';
export { VideoRecorder } from './lib/recorderjs/videoRecorder';
export { chatMessages } from './views/app/room';
diff --git a/app/ui/client/lib/fileUpload.js b/app/ui/client/lib/fileUpload.js
index b8ad23505353a..0ffd21794ee06 100644
--- a/app/ui/client/lib/fileUpload.js
+++ b/app/ui/client/lib/fileUpload.js
@@ -1,13 +1,14 @@
import { Tracker } from 'meteor/tracker';
+import { Random } from 'meteor/random';
import { Session } from 'meteor/session';
import s from 'underscore.string';
import { Handlebars } from 'meteor/ui';
-import { Random } from 'meteor/random';
import { settings } from '../../../settings/client';
-import { t, fileUploadIsValidContentType, APIClient } from '../../../utils';
+import { ChatMessage } from '../../../models/client';
+import { t, fileUploadIsValidContentType, SWCache, APIClient } from '../../../utils';
import { modal, prependReplies } from '../../../ui-utils';
-
+import { sendOfflineFileMessage } from './sendOfflineFileMessage';
const readAsDataURL = (file, callback) => {
const reader = new FileReader();
@@ -214,7 +215,6 @@ export const fileUpload = async (files, input, { rid, tmid }) => {
const replies = $(input).data('reply') || [];
const mention = $(input).data('mention-user') || false;
-
let msg = '';
if (!mention || !threadsEnabled) {
@@ -225,6 +225,9 @@ export const fileUpload = async (files, input, { rid, tmid }) => {
tmid = replies[0]._id;
}
+ const msgData = { id: Random.id(), msg, tmid };
+ let offlineFile = null;
+
const uploadNextFile = () => {
const file = files.pop();
if (!file) {
@@ -266,16 +269,74 @@ export const fileUpload = async (files, input, { rid, tmid }) => {
return;
}
+ const record = {
+ name: document.getElementById('file-name').value || file.name || file.file.name,
+ size: file.file.size,
+ type: file.file.type,
+ rid,
+ description: document.getElementById('file-description').value,
+ };
+
const fileName = document.getElementById('file-name').value || file.name || file.file.name;
- uploadFileWithMessage(rid, tmid, {
- description: document.getElementById('file-description').value || undefined,
- fileName,
- msg: msg || undefined,
- file,
+ const data = new FormData();
+ record.description && data.append('description', record.description);
+ data.append('id', msgData.id);
+ msg && data.append('msg', msg);
+ tmid && data.append('tmid', tmid);
+ data.append('file', file.file, fileName);
+
+ const upload = {
+ id: Random.id(),
+ name: fileName,
+ percentage: 0,
+ };
+ file.file._id = upload.id;
+ uploadNextFile();
+
+ sendOfflineFileMessage(rid, msgData, file.file, record, (file) => {
+ offlineFile = file;
});
- uploadNextFile();
+ const { xhr, promise } = APIClient.upload(`v1/rooms.upload/${ rid }`, {}, data, {
+ progress(progress) {
+ if (progress === 100) {
+ return;
+ }
+
+ const uploads = upload;
+ uploads.percentage = Math.round(progress) || 0;
+ ChatMessage.setProgress(msgData.id, uploads);
+ },
+ error(error) {
+ const uploads = upload;
+ uploads.error = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message;
+ uploads.percentage = 0;
+ ChatMessage.setProgress(msg._id, uploads);
+ },
+ });
+
+ Tracker.autorun((computation) => {
+ const isCanceling = Session.get(`uploading-cancel-${ upload.id }`);
+ if (!isCanceling) {
+ return;
+ }
+ computation.stop();
+ Session.delete(`uploading-cancel-${ upload.id }`);
+
+ xhr.abort();
+ });
+
+ try {
+ const res = await promise;
+
+ if (typeof res === 'object' && res.success && offlineFile) { SWCache.removeFromCache(offlineFile); }
+ } catch (error) {
+ const uploads = upload;
+ uploads.error = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message;
+ uploads.percentage = 0;
+ ChatMessage.setProgress(msgData.id, uploads);
+ }
}));
};
diff --git a/app/ui/client/lib/iframeCommands.js b/app/ui/client/lib/iframeCommands.js
index 5ad9003470b26..e9e2a64c513a0 100644
--- a/app/ui/client/lib/iframeCommands.js
+++ b/app/ui/client/lib/iframeCommands.js
@@ -48,6 +48,7 @@ const commands = {
const customLoginWith = Meteor[`loginWith${ s.capitalize(customOauth.service, true) }`];
const customRedirectUri = data.redirectUrl || siteUrl;
customLoginWith.call(Meteor, { redirectUrl: customRedirectUri }, customOAuthCallback);
+ callbacks.run('onUserLogin');
}
}
},
diff --git a/app/ui/client/lib/notification.js b/app/ui/client/lib/notification.js
index ee8b8918d9001..51f3708359837 100644
--- a/app/ui/client/lib/notification.js
+++ b/app/ui/client/lib/notification.js
@@ -168,5 +168,11 @@ Meteor.startup(() => {
} else {
CustomSounds.pause(newRoomNotification);
}
+
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.onmessage = (event) => {
+ KonchatNotification.showDesktop(event.data);
+ };
+ }
});
});
diff --git a/app/ui/client/lib/sendOfflineFileMessage.js b/app/ui/client/lib/sendOfflineFileMessage.js
new file mode 100644
index 0000000000000..7aba4fb8b65ed
--- /dev/null
+++ b/app/ui/client/lib/sendOfflineFileMessage.js
@@ -0,0 +1,104 @@
+import { Meteor } from 'meteor/meteor';
+import { Random } from 'meteor/random';
+import toastr from 'toastr';
+
+import { ChatMessage } from '../../../models';
+import { settings } from '../../../settings';
+import { callbacks } from '../../../callbacks';
+import { promises } from '../../../promises/client';
+import { t, SWCache } from '../../../utils/client';
+
+const getUrl = ({ _id, name }) => `/file-upload/${ _id }/${ name }`;
+
+const getOfflineMessage = (roomId, msgData, file, meta) => {
+ const id = file._id || Random.id();
+ const name = file.name || meta.name;
+ const type = file.type || meta.type;
+ const fileUrl = getUrl({ _id: id, name });
+ const size = file.size || meta.size;
+ const attachment = {
+ title: name,
+ type: 'file',
+ temp: true,
+ description: file.description || meta.description,
+ title_link: fileUrl,
+ title_link_download: true,
+ };
+
+ if (/^image\/.+/.test(type)) {
+ attachment.image_url = fileUrl;
+ attachment.image_type = type;
+ attachment.image_size = size;
+ if (file.identify && file.identify.size) {
+ attachment.image_dimensions = file.identify.size;
+ }
+ } else if (/^audio\/.+/.test(file.type)) {
+ attachment.audio_url = fileUrl;
+ attachment.audio_type = type;
+ attachment.audio_size = size;
+ } else if (/^video\/.+/.test(file.type)) {
+ attachment.video_url = fileUrl;
+ attachment.video_type = type;
+ attachment.video_size = size;
+ }
+
+ return Object.assign({
+ _id: msgData.id,
+ rid: roomId,
+ ts: new Date(),
+ msg: '',
+ file: {
+ _id: id,
+ name,
+ type,
+ },
+ uploads: {
+ id,
+ name,
+ percentage: 0,
+ },
+ meta,
+ groupable: false,
+ attachments: [attachment],
+ }, msgData);
+};
+
+export const sendOfflineFileMessage = (roomId, msgData, file, meta, callback) => {
+ if (!Meteor.userId()) {
+ return false;
+ }
+ let message = getOfflineMessage(roomId, msgData, file, meta);
+ const messageAlreadyExists = message._id && ChatMessage.findOne({ _id: message._id });
+
+ if (messageAlreadyExists) {
+ return toastr.error(t('Message_Already_Sent'));
+ }
+
+ const user = Meteor.user();
+ message.ts = new Date();
+ message.u = {
+ _id: Meteor.userId(),
+ username: user.username,
+ };
+
+ if (settings.get('UI_Use_Real_Name')) {
+ message.u.name = user.name;
+ }
+
+ message.temp = true;
+ message.tempActions = { send: true };
+ if (settings.get('Message_Read_Receipt_Enabled')) {
+ message.unread = true;
+ }
+
+ SWCache.uploadToCache(message, file, (error) => {
+ if (error) { return; }
+
+ callback(message.file);
+ message = callbacks.run('beforeSaveMessage', message);
+ promises.run('onClientMessageReceived', message).then(function(message) {
+ ChatMessage.insert(message);
+ return callbacks.run('afterSaveMessage', message);
+ });
+ });
+};
diff --git a/app/ui/client/views/app/burger.html b/app/ui/client/views/app/burger.html
index 825dcd4f18c25..f05021230d850 100644
--- a/app/ui/client/views/app/burger.html
+++ b/app/ui/client/views/app/burger.html
@@ -1,5 +1,5 @@
-
-
+
\ No newline at end of file
diff --git a/app/ui/client/views/app/room.js b/app/ui/client/views/app/room.js
index 7914043a6c5fc..9d3bc3ca3b57b 100644
--- a/app/ui/client/views/app/room.js
+++ b/app/ui/client/views/app/room.js
@@ -48,13 +48,13 @@ export const openProfileTab = (e, tabBar, username) => {
tabBar.openUserInfo(username);
};
-const wipeFailedUploads = () => {
- const uploads = Session.get('uploading');
+// const wipeFailedUploads = () => {
+// const uploads = Session.get('uploading');
- if (uploads) {
- Session.set('uploading', uploads.filter((upload) => !upload.error));
- }
-};
+// if (uploads) {
+// Session.set('uploading', uploads.filter((upload) => !upload.error));
+// }
+// };
function roomHasGlobalPurge(room) {
if (!settings.get('RetentionPolicy_Enabled')) {
@@ -157,7 +157,8 @@ function addToInput(text) {
$(input).change().trigger('input');
}
-callbacks.add('enter-room', wipeFailedUploads);
+// WIDE CHAT
+// callbacks.add('enter-room', wipeFailedUploads);
export const dropzoneHelpers = {
dragAndDrop() {
@@ -186,6 +187,7 @@ Template.roomOld.helpers({
},
subscribed() {
const { state } = Template.instance();
+ console.log(state);
return state.get('subscribed');
},
messagesHistory() {
@@ -215,7 +217,6 @@ Template.roomOld.helpers({
ts: 1,
},
};
-
return ChatMessage.find(query, options);
},
@@ -235,10 +236,6 @@ Template.roomOld.helpers({
return `chat-window-${ this._id }`;
},
- uploading() {
- return Session.get('uploading');
- },
-
roomLeader() {
const roles = RoomRoles.findOne({ rid: this._id, roles: 'leader', 'u._id': { $ne: Meteor.userId() } });
if (roles) {
diff --git a/app/utils/client/index.js b/app/utils/client/index.js
index 6df40269d05e2..becb742e64b16 100644
--- a/app/utils/client/index.js
+++ b/app/utils/client/index.js
@@ -18,4 +18,9 @@ export { placeholders } from '../lib/placeholders';
export { templateVarHandler } from '../lib/templateVarHandler';
export { APIClient, mountArrayQueryParameters } from './lib/RestApiClient';
export { canDeleteMessage } from './lib/canDeleteMessage';
+export { SWCache } from './lib/swCache';
+export { cleanMessagesAtStartup, triggerOfflineMsgs } from './lib/offlineMessages';
export { secondsToHHMMSS } from '../lib/timeConverter';
+export { isMobile } from './lib/isMobile';
+export { hex_sha1 } from './lib/sha1';
+export { share, isShareAvailable, getShareData } from './lib/share';
diff --git a/app/utils/client/lib/isMobile.js b/app/utils/client/lib/isMobile.js
new file mode 100644
index 0000000000000..94bb8cb74b00c
--- /dev/null
+++ b/app/utils/client/lib/isMobile.js
@@ -0,0 +1,6 @@
+export const isMobile = () => {
+ if (window.matchMedia('(max-width: 500px)').matches) {
+ return true;
+ }
+ return false;
+};
diff --git a/app/utils/client/lib/offlineMessages.js b/app/utils/client/lib/offlineMessages.js
new file mode 100644
index 0000000000000..675f8718fa74b
--- /dev/null
+++ b/app/utils/client/lib/offlineMessages.js
@@ -0,0 +1,157 @@
+import { Tracker } from 'meteor/tracker';
+import { Session } from 'meteor/session';
+import { sortBy } from 'underscore';
+import localforage from 'localforage';
+
+import { call } from '../../../ui-utils/client';
+import { getConfig } from '../../../ui-utils/client/config';
+import { ChatMessage, CachedChatMessage } from '../../../models/client';
+import { SWCache, APIClient } from '..';
+
+const action = {
+ clean: (msg) => {
+ const { temp, tempActions, ...originalMsg } = msg;
+ return originalMsg;
+ },
+
+ send: (msg) => {
+ msg.ts = new Date();
+ if (msg.file && msg.meta) {
+ action.sendFile(msg);
+ return;
+ }
+
+ call('sendMessage', msg, true);
+ },
+
+ sendFile: async (msg) => {
+ const file = await SWCache.getFileFromCache(msg.file);
+ const upload = {
+ id: msg.file._id,
+ name: msg.file.name,
+ percentage: 0,
+ };
+
+ if (!file) { return; }
+
+ const data = new FormData();
+ msg.meta.description && data.append('description', msg.meta.description);
+ data.append('id', msg._id);
+ msg.msg && data.append('msg', msg.msg);
+ msg.tmid && data.append('tmid', msg.tmid);
+ data.append('file', file, msg.file.name);
+ const { xhr, promise } = APIClient.upload(`v1/rooms.upload/${ msg.rid }`, {}, data, {
+ progress(progress) {
+ if (progress === 100) {
+ return;
+ }
+ const uploads = upload;
+ uploads.percentage = Math.round(progress) || 0;
+ ChatMessage.setProgress(msg._id, uploads);
+ },
+ error(error) {
+ const uploads = upload;
+ uploads.error = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message;
+ uploads.percentage = 0;
+ ChatMessage.setProgress(msg._id, uploads);
+ },
+ });
+
+ Tracker.autorun((computation) => {
+ const isCanceling = Session.get(`uploading-cancel-${ upload.id }`);
+
+ if (!isCanceling) {
+ return;
+ }
+ computation.stop();
+ Session.delete(`uploading-cancel-${ upload.id }`);
+
+ xhr.abort();
+ });
+
+ try {
+ const res = await promise;
+
+ if (typeof res === 'object' && res.success) { SWCache.removeFromCache(msg.file); }
+ } catch (error) {
+ const uploads = upload;
+ uploads.error = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message;
+ uploads.percentage = 0;
+ ChatMessage.setProgress(msg._id, uploads);
+ }
+ },
+
+ update: (msg) => {
+ msg.editedAt = new Date();
+ call('updateMessage', msg, true);
+ },
+
+ react: ({ _id }, reaction) => {
+ call('setReaction', reaction, _id, undefined, true);
+ },
+
+ delete: ({ _id }) => call('deleteMessage', { _id }, true),
+};
+
+function trigger(msg) {
+ const tempActions = msg.tempActions || {};
+ msg = action.clean(msg);
+
+ if (tempActions.send) {
+ action.send(msg);
+ return;
+ }
+
+ if (tempActions.delete) {
+ action.delete(msg);
+ return;
+ }
+
+ if (tempActions.update) {
+ action.update(msg);
+ }
+
+ if (tempActions.react && tempActions.reactions) {
+ tempActions.reactions.forEach((reaction) => {
+ action.react(msg, reaction);
+ });
+ }
+}
+
+export const triggerOfflineMsgs = () => {
+ localforage.getItem('chatMessage').then((value) => {
+ if (value && value.records) {
+ const tempMsgs = value.records.filter((msg) => msg.temp);
+ tempMsgs.forEach((msg) => trigger(msg));
+ }
+ });
+};
+
+const retainMessages = (rid, messages) => {
+ const roomMsgs = messages.filter((msg) => rid === msg.rid);
+ const limit = parseInt(getConfig('roomListLimit')) || 50;
+ const retain = sortBy(roomMsgs.filter((msg) => !msg.temp), 'ts').reverse().slice(0, limit);
+ retain.push(...roomMsgs.filter((msg) => msg.temp));
+ return retain;
+};
+
+function clearOldMessages({ records: messages, ...value }) {
+ const rids = [...new Set(messages.map((msg) => msg.rid))];
+ const retain = [];
+ rids.forEach((rid) => {
+ retain.push(...retainMessages(rid, messages));
+ });
+ value.records = retain;
+ value.updatedAt = new Date();
+ localforage.setItem('chatMessage', value).then(() => {
+ CachedChatMessage.loadFromCache();
+ });
+}
+
+export const cleanMessagesAtStartup = () => {
+ localforage.getItem('chatMessage').then((value) => {
+ if (value && value.records) {
+ clearOldMessages(value);
+ }
+ });
+};
diff --git a/app/utils/client/lib/sha1.js b/app/utils/client/lib/sha1.js
new file mode 100644
index 0000000000000..8c86f9c81949f
--- /dev/null
+++ b/app/utils/client/lib/sha1.js
@@ -0,0 +1,330 @@
+/*
+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
+ * in FIPS 180-1
+ * Version 2.2 Copyright Paul Johnston 2000 - 2009.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for details.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+export const hex_sha1 = function(s) { return rstr2hex(rstr_sha1(str2rstr_utf8(s))); }
+function b64_sha1(s) { return rstr2b64(rstr_sha1(str2rstr_utf8(s))); }
+function any_sha1(s, e) { return rstr2any(rstr_sha1(str2rstr_utf8(s)), e); }
+function hex_hmac_sha1(k, d)
+ { return rstr2hex(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
+function b64_hmac_sha1(k, d)
+ { return rstr2b64(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d))); }
+function any_hmac_sha1(k, d, e)
+ { return rstr2any(rstr_hmac_sha1(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function sha1_vm_test()
+{
+ return hex_sha1("abc").toLowerCase() == "a9993e364706816aba3e25717850c26c9cd0d89d";
+}
+
+/*
+ * Calculate the SHA1 of a raw string
+ */
+function rstr_sha1(s)
+{
+ return binb2rstr(binb_sha1(rstr2binb(s), s.length * 8));
+}
+
+/*
+ * Calculate the HMAC-SHA1 of a key and some data (raw strings)
+ */
+function rstr_hmac_sha1(key, data)
+{
+ var bkey = rstr2binb(key);
+ if(bkey.length > 16) bkey = binb_sha1(bkey, key.length * 8);
+
+ var ipad = Array(16), opad = Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5C5C5C5C;
+ }
+
+ var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8);
+ return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160));
+}
+
+/*
+ * Convert a raw string to a hex string
+ */
+function rstr2hex(input)
+{
+ try { hexcase } catch(e) { hexcase=0; }
+ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var output = "";
+ var x;
+ for(var i = 0; i < input.length; i++)
+ {
+ x = input.charCodeAt(i);
+ output += hex_tab.charAt((x >>> 4) & 0x0F)
+ + hex_tab.charAt( x & 0x0F);
+ }
+ return output;
+}
+
+/*
+ * Convert a raw string to a base-64 string
+ */
+function rstr2b64(input)
+{
+ try { b64pad } catch(e) { b64pad=''; }
+ var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var output = "";
+ var len = input.length;
+ for(var i = 0; i < len; i += 3)
+ {
+ var triplet = (input.charCodeAt(i) << 16)
+ | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
+ | (i + 2 < len ? input.charCodeAt(i+2) : 0);
+ for(var j = 0; j < 4; j++)
+ {
+ if(i * 8 + j * 6 > input.length * 8) output += b64pad;
+ else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
+ }
+ }
+ return output;
+}
+
+/*
+ * Convert a raw string to an arbitrary string encoding
+ */
+function rstr2any(input, encoding)
+{
+ var divisor = encoding.length;
+ var remainders = Array();
+ var i, q, x, quotient;
+
+ /* Convert to an array of 16-bit big-endian values, forming the dividend */
+ var dividend = Array(Math.ceil(input.length / 2));
+ for(i = 0; i < dividend.length; i++)
+ {
+ dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
+ }
+
+ /*
+ * Repeatedly perform a long division. The binary array forms the dividend,
+ * the length of the encoding is the divisor. Once computed, the quotient
+ * forms the dividend for the next step. We stop when the dividend is zero.
+ * All remainders are stored for later use.
+ */
+ while(dividend.length > 0)
+ {
+ quotient = Array();
+ x = 0;
+ for(i = 0; i < dividend.length; i++)
+ {
+ x = (x << 16) + dividend[i];
+ q = Math.floor(x / divisor);
+ x -= q * divisor;
+ if(quotient.length > 0 || q > 0)
+ quotient[quotient.length] = q;
+ }
+ remainders[remainders.length] = x;
+ dividend = quotient;
+ }
+
+ /* Convert the remainders to the output string */
+ var output = "";
+ for(i = remainders.length - 1; i >= 0; i--)
+ output += encoding.charAt(remainders[i]);
+
+ /* Append leading zero equivalents */
+ var full_length = Math.ceil(input.length * 8 /
+ (Math.log(encoding.length) / Math.log(2)))
+ for(i = output.length; i < full_length; i++)
+ output = encoding[0] + output;
+
+ return output;
+}
+
+/*
+ * Encode a string as utf-8.
+ * For efficiency, this assumes the input is valid utf-16.
+ */
+function str2rstr_utf8(input)
+{
+ var output = "";
+ var i = -1;
+ var x, y;
+
+ while(++i < input.length)
+ {
+ /* Decode utf-16 surrogate pairs */
+ x = input.charCodeAt(i);
+ y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
+ if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
+ {
+ x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
+ i++;
+ }
+
+ /* Encode output as utf-8 */
+ if(x <= 0x7F)
+ output += String.fromCharCode(x);
+ else if(x <= 0x7FF)
+ output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
+ 0x80 | ( x & 0x3F));
+ else if(x <= 0xFFFF)
+ output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
+ 0x80 | ((x >>> 6 ) & 0x3F),
+ 0x80 | ( x & 0x3F));
+ else if(x <= 0x1FFFFF)
+ output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
+ 0x80 | ((x >>> 12) & 0x3F),
+ 0x80 | ((x >>> 6 ) & 0x3F),
+ 0x80 | ( x & 0x3F));
+ }
+ return output;
+}
+
+/*
+ * Encode a string as utf-16
+ */
+function str2rstr_utf16le(input)
+{
+ var output = "";
+ for(var i = 0; i < input.length; i++)
+ output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
+ (input.charCodeAt(i) >>> 8) & 0xFF);
+ return output;
+}
+
+function str2rstr_utf16be(input)
+{
+ var output = "";
+ for(var i = 0; i < input.length; i++)
+ output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
+ input.charCodeAt(i) & 0xFF);
+ return output;
+}
+
+/*
+ * Convert a raw string to an array of big-endian words
+ * Characters >255 have their high-byte silently ignored.
+ */
+function rstr2binb(input)
+{
+ var output = Array(input.length >> 2);
+ for(var i = 0; i < output.length; i++)
+ output[i] = 0;
+ for(var i = 0; i < input.length * 8; i += 8)
+ output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32);
+ return output;
+}
+
+/*
+ * Convert an array of big-endian words to a string
+ */
+function binb2rstr(input)
+{
+ var output = "";
+ for(var i = 0; i < input.length * 32; i += 8)
+ output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF);
+ return output;
+}
+
+/*
+ * Calculate the SHA-1 of an array of big-endian words, and a bit length
+ */
+function binb_sha1(x, len)
+{
+ /* append padding */
+ x[len >> 5] |= 0x80 << (24 - len % 32);
+ x[((len + 64 >> 9) << 4) + 15] = len;
+
+ var w = Array(80);
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+ var e = -1009589776;
+
+ for(var i = 0; i < x.length; i += 16)
+ {
+ var olda = a;
+ var oldb = b;
+ var oldc = c;
+ var oldd = d;
+ var olde = e;
+
+ for(var j = 0; j < 80; j++)
+ {
+ if(j < 16) w[j] = x[i + j];
+ else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
+ var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)),
+ safe_add(safe_add(e, w[j]), sha1_kt(j)));
+ e = d;
+ d = c;
+ c = bit_rol(b, 30);
+ b = a;
+ a = t;
+ }
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ e = safe_add(e, olde);
+ }
+ return Array(a, b, c, d, e);
+
+}
+
+/*
+ * Perform the appropriate triplet combination function for the current
+ * iteration
+ */
+function sha1_ft(t, b, c, d)
+{
+ if(t < 20) return (b & c) | ((~b) & d);
+ if(t < 40) return b ^ c ^ d;
+ if(t < 60) return (b & c) | (b & d) | (c & d);
+ return b ^ c ^ d;
+}
+
+/*
+ * Determine the appropriate additive constant for the current iteration
+ */
+function sha1_kt(t)
+{
+ return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
+ (t < 60) ? -1894007588 : -899497514;
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function bit_rol(num, cnt)
+{
+ return (num << cnt) | (num >>> (32 - cnt));
+}
diff --git a/app/utils/client/lib/share.js b/app/utils/client/lib/share.js
new file mode 100644
index 0000000000000..b8059290a67c6
--- /dev/null
+++ b/app/utils/client/lib/share.js
@@ -0,0 +1,55 @@
+import { Meteor } from 'meteor/meteor';
+
+// TODO: Remove logs
+
+export const isShareAvailable = () => {
+ if (navigator.share) { return true; }
+ return false;
+};
+
+export const getShareData = () => {
+ const data = {};
+
+ data.url = document.location.href || 'https://viasatconnect.com';
+ const path = new URL(data.url).pathname;
+ const roomName = path.substring(path.lastIndexOf('/') + 1);
+
+ data.title = 'Viasat Connect';
+ data.text = 'Viasat Connect is a new application that makes it easy for you to chat with friends and family. Open this link to connect.';
+
+ if (path.startsWith('/channel')) {
+ data.title = `Join #${ roomName } on Viasat Connect`;
+ data.text = `You are invited to channel #${ roomName } on Viasat Connect. ${ data.text }`;
+ } else if (path.startsWith('/group')) {
+ data.title = `Join #${ roomName } on Viasat Connect`;
+ data.text = `You are invited to private group 🔒${ roomName } on Viasat Connect. ${ data.text }`;
+ } else if (path.startsWith('/direct')) {
+ data.title = `Chat with @${ roomName } on Viasat Connect`;
+ } else {
+ const user = Meteor.user();
+
+ data.title = 'Viasat Connect';
+ data.text = 'Viasat Connect is a new application that makes it easy for me to chat with friends and family. Open this link and connect with me.';
+ data.url = new URL(document.location.href).origin;
+
+ if (data.url && user) {
+ data.url = `${ data.url }/direct/${ user.username }`;
+ }
+ }
+
+ return data;
+};
+
+export const share = () => {
+ const data = getShareData();
+
+ console.log(`data: ${ JSON.stringify(data) }`);
+
+ if (navigator.share) {
+ navigator.share(data)
+ .then(() => console.log('Successfully shared'))
+ .catch((error) => console.log('Error while sharing', error));
+ } else {
+ console.log('Share feature not available');
+ }
+};
diff --git a/app/utils/client/lib/swCache.js b/app/utils/client/lib/swCache.js
new file mode 100644
index 0000000000000..93912dde9ad7e
--- /dev/null
+++ b/app/utils/client/lib/swCache.js
@@ -0,0 +1,34 @@
+const version = 'viasat-0.1';
+const getFileUrl = ({ _id, name }) => `/file-upload/${ _id }/${ name }`;
+
+export const SWCache = {
+ uploadToCache: (message, file, callback) => {
+ caches.open(version).then((cache) => {
+ file._id = file._id || message.file._id;
+ file.name = file.name || message.file.name;
+ const res = new Response(file, {
+ status: 200,
+ statusText: 'No connection to the server',
+ headers: new Headers({ 'Content-Type': file.type }),
+ });
+ cache.put(getFileUrl(file), res).then(() => {
+ callback();
+ });
+ }).catch((err) => {
+ callback(err);
+ });
+ },
+
+ removeFromCache: (file) => {
+ caches.open(version).then((cache) => {
+ cache.delete(getFileUrl(file));
+ }).catch((err) => {
+ console.log(err);
+ });
+ },
+
+ getFileFromCache: ({ _id, name, type }) => fetch(getFileUrl({ _id, name }))
+ .then((r) => r.blob())
+ .then((blobFile) => new File([blobFile], name, { type })),
+
+};
diff --git a/app/utils/lib/placeholders.js b/app/utils/lib/placeholders.js
index ffb93e7312aeb..46165960ec2fa 100644
--- a/app/utils/lib/placeholders.js
+++ b/app/utils/lib/placeholders.js
@@ -1,6 +1,8 @@
+import { Meteor } from 'meteor/meteor';
import s from 'underscore.string';
import { settings } from '../../settings';
+import { getAvatarURL } from './getAvatarURL';
export const placeholders = {
replace: (str, data) => {
@@ -11,6 +13,19 @@ export const placeholders = {
str = str.replace(/\[Site_Name\]/g, settings.get('Site_Name') || '');
str = str.replace(/\[Site_URL\]/g, settings.get('Site_Url') || '');
+ if (str.includes('[Invite_Link]')) {
+ const invite_link = Meteor.runAsUser(Meteor.userId(), () => Meteor.call('getInviteLink'));
+ str = str.replace(/\[Invite_Link\]/g, invite_link);
+ }
+
+ if (str.includes('[Username]')) {
+ str = str.replace(/\[Username\]/g, Meteor.user().username);
+ }
+
+ if (str.includes('[Avatar_Link]')) {
+ str = str.replace(/\[Avatar_Link\]/g, `${ settings.get('Site_Url').slice(0, -1) }${ getAvatarURL(Meteor.user().username) }`);
+ }
+
if (data) {
str = str.replace(/\[name\]/g, data.name || '');
str = str.replace(/\[fname\]/g, s.strLeft(data.name, ' ') || '');
diff --git a/client/components/AutoCompleteDepartment.js b/client/components/AutoCompleteDepartment.js
index c7a4b83f7dc34..e4ba6fa768edb 100644
--- a/client/components/AutoCompleteDepartment.js
+++ b/client/components/AutoCompleteDepartment.js
@@ -44,7 +44,7 @@ const AutoCompleteDepartment = (props) => {
});
const department = sortedByName.find(
- (dep) => dep._id === (typeof value === 'string' ? value : value.value),
+ (dep) => dep._id === (typeof value === 'string' ? value : value?.value),
)?.value;
return (
diff --git a/client/components/SortList/SortList.js b/client/components/SortList/SortList.js
index 3aaca9bd88eef..39d5d12059859 100644
--- a/client/components/SortList/SortList.js
+++ b/client/components/SortList/SortList.js
@@ -1,6 +1,7 @@
import { Divider } from '@rocket.chat/fuselage';
import React from 'react';
+import { isMobile } from '../../../app/utils/client';
import GroupingList from './GroupingList';
import SortModeList from './SortModeList';
import ViewModeList from './ViewModeList';
@@ -9,8 +10,8 @@ function SortList() {
return (
<>
-
-
+ {!isMobile() &&
}
+ {!isMobile() &&
}
diff --git a/client/head.html b/client/head.html
index e92d2c10606a6..78941e3d982c0 100644
--- a/client/head.html
+++ b/client/head.html
@@ -13,7 +13,11 @@
-
+
+
+
+
+
diff --git a/client/hooks/useSession.js b/client/hooks/useSession.js
new file mode 100644
index 0000000000000..731d222651053
--- /dev/null
+++ b/client/hooks/useSession.js
@@ -0,0 +1,5 @@
+import { Session } from 'meteor/session';
+
+import { useReactiveValue } from './useReactiveValue';
+
+export const useSession = (variableName) => useReactiveValue(() => Session.get(variableName));
diff --git a/client/hooks/useUserId.js b/client/hooks/useUserId.js
new file mode 100644
index 0000000000000..600efa45f5117
--- /dev/null
+++ b/client/hooks/useUserId.js
@@ -0,0 +1,5 @@
+import { Meteor } from 'meteor/meteor';
+
+import { useReactiveValue } from './useReactiveValue';
+
+export const useUserId = () => useReactiveValue(() => Meteor.userId());
diff --git a/client/hooks/useUserPreference.js b/client/hooks/useUserPreference.js
new file mode 100644
index 0000000000000..b3ef7412ed452
--- /dev/null
+++ b/client/hooks/useUserPreference.js
@@ -0,0 +1,8 @@
+import { getUserPreference } from '../../app/utils/client';
+import { useReactiveValue } from './useReactiveValue';
+import { useUserId } from './useUserId';
+
+export const useUserPreference = (key, defaultValue = undefined) => {
+ const userId = useUserId();
+ return useReactiveValue(() => getUserPreference(userId, key, defaultValue), [userId]);
+};
diff --git a/client/importPackages.ts b/client/importPackages.ts
index a6c7c487b9874..3c560634be9aa 100644
--- a/client/importPackages.ts
+++ b/client/importPackages.ts
@@ -30,7 +30,7 @@ import '../app/lib/client';
import '../app/livestream/client';
import '../app/logger/client';
import '../app/token-login/client';
-import '../app/markdown/client';
+// import '../app/markdown/client';
import '../app/mentions-flextab/client';
import '../app/message-attachments/client';
import '../app/message-mark-as-unread/client';
@@ -63,6 +63,7 @@ import '../app/ui-login/client';
import '../app/ui-master/client';
import '../app/ui-message/client';
import '../app/ui-sidenav/client';
+import '../app/ui-share/client';
import '../app/ui-vrecord/client';
import '../app/videobridge/client';
import '../app/webdav/client';
diff --git a/client/importServiceWorker.js b/client/importServiceWorker.js
new file mode 100644
index 0000000000000..7009191e0fdf9
--- /dev/null
+++ b/client/importServiceWorker.js
@@ -0,0 +1,110 @@
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+
+import { settings } from '../app/settings/client';
+import { modal } from '../app/ui-utils/client';
+import { handleError, t } from '../app/utils/client';
+
+function urlBase64ToUint8Array(base64String) {
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
+
+ const rawData = atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+}
+
+function isMobile() {
+ const toMatch = [
+ /Android/i,
+ /webOS/i,
+ /iPhone/i,
+ /iPad/i,
+ /iPod/i,
+ /BlackBerry/i,
+ /Windows Phone/i,
+ ];
+
+ return toMatch.some((toMatchItem) => navigator.userAgent.match(toMatchItem));
+}
+
+function subscribeUser() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then(async (reg) => {
+ try {
+ const vapidKey = await settings.get('Vapid_public_key');
+ const subscription = await reg.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(vapidKey),
+ });
+
+ const platform = isMobile() ? 'mobile' : 'desktop';
+ Meteor.call('savePushNotificationSubscription', JSON.stringify(subscription), platform);
+ } catch (e) {
+ handleError(e);
+ }
+ });
+ }
+}
+
+Meteor.startup(() => {
+ Tracker.autorun((computation) => {
+ const settingsReady = settings.cachedCollection.ready.get();
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker
+ .register('sw.js', {
+ scope: './',
+ })
+ .then((reg) => {
+ if (reg.installing) {
+ const sw = reg.installing || reg.waiting;
+ sw.onstatechange = function () {
+ if (sw.state === 'installed') {
+ // SW installed. Reload page.
+ window.location.reload();
+ }
+ };
+ console.log(`Service worker has been registered for scope: ${reg.scope}`);
+ } else {
+ if (settings.get('Push_enable') !== true) {
+ return;
+ }
+
+ reg.pushManager.getSubscription().then((sub) => {
+ if (sub === null) {
+ console.log('Not subscribed to push service!');
+ if (settingsReady) {
+ modal.open(
+ {
+ title: t('Important'),
+ type: 'info',
+ text: t('Please subscribe to push notifications to continue'),
+ showCancelButton: true,
+ confirmButtonText: t('Subscribe'),
+ cancelButtonText: t('Cancel'),
+ closeOnConfirm: true,
+ },
+ () => {
+ Notification.requestPermission().then((permission) => {
+ if (permission === 'granted') {
+ subscribeUser();
+ }
+ });
+ },
+ );
+ computation.stop();
+ }
+ } else {
+ console.log('Subscribed to push service');
+ computation.stop();
+ }
+ });
+ }
+ });
+ }
+ });
+});
diff --git a/client/main.ts b/client/main.ts
index e08f775af7dee..a22900124f2c7 100644
--- a/client/main.ts
+++ b/client/main.ts
@@ -1,6 +1,8 @@
import '../ee/client/ecdh';
import './polyfills';
+import './importServiceWorker';
+
import './lib/meteorCallWrapper';
import './importPackages';
diff --git a/client/methods/deleteMessage.js b/client/methods/deleteMessage.js
index dad84129af205..6c1b0554bcc8e 100644
--- a/client/methods/deleteMessage.js
+++ b/client/methods/deleteMessage.js
@@ -1,11 +1,12 @@
import { Meteor } from 'meteor/meteor';
+import { Session } from 'meteor/session';
import { ChatMessage } from '../../app/models/client';
-import { canDeleteMessage } from '../../app/utils/client';
+import { canDeleteMessage, SWCache } from '../../app/utils/client';
Meteor.methods({
- deleteMessage(msg) {
- if (!Meteor.userId()) {
+ deleteMessage(msg, offlineTriggered = false) {
+ if (!Meteor.userId() || offlineTriggered) {
return false;
}
@@ -13,6 +14,7 @@ Meteor.methods({
const message = ChatMessage.findOne({ _id: msg._id });
if (
+ !message ||
!canDeleteMessage({
rid: message.rid,
ts: message.ts,
@@ -22,9 +24,25 @@ Meteor.methods({
return false;
}
- ChatMessage.remove({
- '_id': message._id,
- 'u._id': Meteor.userId(),
- });
+ if (message.temp && message.tempActions.send) {
+ ChatMessage.remove({
+ '_id': message._id,
+ 'u._id': Meteor.userId(),
+ });
+ if (message.file) {
+ SWCache.removeFromCache(message.file);
+ Session.set(`uploading-cancel-${message.file._id}`, true);
+ }
+ } else {
+ const messageObject = { temp: true, msg: 'Message deleted', tempActions: { delete: true } };
+
+ ChatMessage.update(
+ {
+ '_id': message._id,
+ 'u._id': Meteor.userId(),
+ },
+ { $set: messageObject, $unset: { reactions: 1, file: 1, attachments: 1 } },
+ );
+ }
},
});
diff --git a/client/methods/updateMessage.js b/client/methods/updateMessage.js
index 70346b7295bf7..153e3ef1cd9e9 100644
--- a/client/methods/updateMessage.js
+++ b/client/methods/updateMessage.js
@@ -12,8 +12,8 @@ import { settings } from '../../app/settings/client';
import { t } from '../../app/utils/client';
Meteor.methods({
- updateMessage(message) {
- if (!Meteor.userId()) {
+ updateMessage(message, offlineTriggered = false) {
+ if (!Meteor.userId() || offlineTriggered) {
return false;
}
@@ -63,10 +63,19 @@ Meteor.methods({
};
message = callbacks.run('beforeSaveMessage', message);
+
+ const tempActions = originalMessage.tempActions || {};
+
+ if (!tempActions.send) {
+ tempActions.update = true;
+ }
+
const messageObject = {
editedAt: message.editedAt,
editedBy: message.editedBy,
msg: message.msg,
+ temp: true,
+ tempActions,
};
if (originalMessage.attachments) {
diff --git a/client/providers/UserProvider.tsx b/client/providers/UserProvider.tsx
index 45a91b6adf97a..c37c97d338c35 100644
--- a/client/providers/UserProvider.tsx
+++ b/client/providers/UserProvider.tsx
@@ -26,6 +26,7 @@ const loginWithPassword = (user: string | object, password: string): Promise
{
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then((reg) => {
+ reg.pushManager.getSubscription().then((sub) => {
+ Meteor.call('removeUserFromPushSubscription', sub.endpoint);
+ });
+ });
+ }
+};
+
+const addUserIdOnLogin = async () => {
+ const user = await Meteor.user();
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then((reg) => {
+ reg.pushManager.getSubscription().then((sub) => {
+ Meteor.call('addUserToPushSubscription', sub.endpoint, user);
+ });
+ });
+ }
+};
+
+callbacks.add(
+ 'afterLogoutCleanUp',
+ removeUserIdOnLogout,
+ callbacks.priority.MEDIUM,
+ 'remove-user-from-push-subscription',
+);
+callbacks.add(
+ 'onUserLogin',
+ addUserIdOnLogin,
+ callbacks.priority.MEDIUM,
+ 'add-user-to-push-subscription',
+);
diff --git a/client/startup/routes.ts b/client/startup/routes.ts
index cd29aaeb8ab10..b13d41dae0a82 100644
--- a/client/startup/routes.ts
+++ b/client/startup/routes.ts
@@ -5,7 +5,7 @@ import { Tracker } from 'meteor/tracker';
import { lazy } from 'react';
import toastr from 'toastr';
-import { KonchatNotification } from '../../app/ui/client';
+// import { KonchatNotification } from '../../app/ui/client';
import { handleError } from '../../app/utils/client';
import { IUser } from '../../definition/IUser';
import { appLayout } from '../lib/appLayout';
@@ -54,7 +54,8 @@ FlowRouter.route('/home', {
name: 'home',
action(_params, queryParams) {
- KonchatNotification.getDesktopPermission();
+ // WIDECHAT
+ // KonchatNotification.getDesktopPermission();
if (queryParams?.saml_idp_credentialToken !== undefined) {
const token = queryParams.saml_idp_credentialToken;
FlowRouter.setQueryParams({
diff --git a/client/startup/startup.ts b/client/startup/startup.ts
index 2cc59de05437e..515b9a15fa0fe 100644
--- a/client/startup/startup.ts
+++ b/client/startup/startup.ts
@@ -42,9 +42,6 @@ Meteor.startup(() => {
if (!uid) {
return;
}
- if (!Meteor.status().connected) {
- return;
- }
const user = await synchronizeUserData(uid);
diff --git a/client/views/account/AccountProfileForm.js b/client/views/account/AccountProfileForm.js
index 86d49a775fa39..4b085b26dcf47 100644
--- a/client/views/account/AccountProfileForm.js
+++ b/client/views/account/AccountProfileForm.js
@@ -151,9 +151,8 @@ function AccountProfileForm({ values, handlers, user, settings, onSaveStateChang
: t('Max_length_is', STATUS_TEXT_MAX_LENGTH),
[statusText, t],
);
- const {
- emails: [{ verified = false } = { verified: false }],
- } = user;
+
+ const verified = (user.emails && user.emails.length && user.emails[0].verified) || false;
const canSave = !![
!!passwordError,
diff --git a/client/views/admin/apps/AppDetailsPageContent.tsx b/client/views/admin/apps/AppDetailsPageContent.tsx
index 1fd8d884d26dc..efa3bd2b89a2d 100644
--- a/client/views/admin/apps/AppDetailsPageContent.tsx
+++ b/client/views/admin/apps/AppDetailsPageContent.tsx
@@ -4,6 +4,7 @@ import React, { FC } from 'react';
import ExternalLink from '../../../components/ExternalLink';
import AppAvatar from '../../../components/avatar/AppAvatar';
+import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { TranslationKey, useTranslation } from '../../../contexts/TranslationContext';
import AppMenu from './AppMenu';
import AppStatus from './AppStatus';
@@ -16,10 +17,11 @@ type AppDetailsPageContentProps = {
const AppDetailsPageContent: FC = ({ data }) => {
const t = useTranslation();
-
+ const dispatchToastMessage = useToastMessageDispatch();
const {
iconFileData = '',
name,
+ commitHash,
author: { name: authorName, homepage, support },
description,
categories = [],
@@ -32,6 +34,11 @@ const AppDetailsPageContent: FC = ({ data }) => {
bundledIn,
} = data;
+ const copyHash = (): void => {
+ navigator.clipboard.writeText(commitHash);
+ dispatchToastMessage({ type: 'success', message: t('Commit_hash_copy_toast') });
+ };
+
return (
<>
@@ -48,6 +55,16 @@ const AppDetailsPageContent: FC = ({ data }) => {
{t('By_author', { author: authorName })}
|{t('Version_version', { version })}
+ {commitHash && (
+
+ | {commitHash.substring(0, 7)}
+
+ )}
{
return;
}
+ const localApp = await Apps.getApp(appId);
const app = apps.find((app) => app.id === appId) ?? {
- ...(await Apps.getApp(appId)),
+ ...localApp,
installed: true,
marketplace: false,
};
+ app.commitHash = localApp.commitHash;
const [bundledIn, settings, apis] = await Promise.all([
app.marketplace === false ? [] : getBundledIn(app.id, app.version),
diff --git a/client/views/admin/apps/types.ts b/client/views/admin/apps/types.ts
index 5f49372dbd39d..64dff15c155ca 100644
--- a/client/views/admin/apps/types.ts
+++ b/client/views/admin/apps/types.ts
@@ -4,6 +4,7 @@ export type App = {
id: string;
iconFileData: string;
name: string;
+ commitHash: string;
author: {
name: string;
homepage: string;
diff --git a/client/views/admin/users/UserForm.js b/client/views/admin/users/UserForm.js
index 8729d6de9b81e..634f3c4dd0511 100644
--- a/client/views/admin/users/UserForm.js
+++ b/client/views/admin/users/UserForm.js
@@ -199,7 +199,7 @@ export default function UserForm({
{
>
{t('Name')}
,
+ {t('Id')},
{
qa-user-id={_id}
>
{fname}
+ {_id}
{department ? department.name : ''}
{servedBy && servedBy.username}
{moment(ts).format('L LTS')}
diff --git a/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js b/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js
index 28aaa7ccc84f9..bf3644f939ef9 100644
--- a/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js
+++ b/client/views/omnichannel/directory/chats/contextualBar/DepartmentField.js
@@ -14,11 +14,11 @@ const DepartmentField = ({ departmentId }) => {
if (state === AsyncStatePhase.LOADING) {
return ;
}
- const { department: { name } = {} } = data || { department: {} };
+ const { department } = data || { department: {} };
return (
- {name || t('Department_not_found')}
+ {department?.name || t('Department_not_found')}
);
};
diff --git a/client/views/omnichannel/filters/EditFilterPage.js b/client/views/omnichannel/filters/EditFilterPage.js
new file mode 100644
index 0000000000000..fadf3c670298e
--- /dev/null
+++ b/client/views/omnichannel/filters/EditFilterPage.js
@@ -0,0 +1,64 @@
+import { Margins, FieldGroup, Box, Button } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React from 'react';
+
+import { useRoute } from '../../../contexts/RouterContext';
+import { useMethod } from '../../../contexts/ServerContext';
+import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useForm } from '../../../hooks/useForm';
+import FiltersForm from './FiltersForm';
+
+const getInitialValues = ({ name, description, enabled, regex, slug }) => ({
+ name: name ?? '',
+ description: description ?? '',
+ enabled: !!enabled,
+ regex: regex ?? '',
+ slug: slug ?? '',
+});
+
+const EditFilterPage = ({ data, onSave }) => {
+ const dispatchToastMessage = useToastMessageDispatch();
+ const t = useTranslation();
+
+ const router = useRoute('omnichannel-filters');
+
+ const save = useMethod('livechat:saveFilter');
+
+ const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data));
+
+ const handleSave = useMutableCallback(async () => {
+ try {
+ await save({
+ _id: data._id,
+ ...values,
+ });
+ dispatchToastMessage({ type: 'success', message: t('Saved') });
+ onSave();
+ router.push({});
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ });
+
+ const { name } = values;
+
+ const canSave = name && hasUnsavedChanges;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default EditFilterPage;
diff --git a/client/views/omnichannel/filters/EditFilterPageContainer.js b/client/views/omnichannel/filters/EditFilterPageContainer.js
new file mode 100644
index 0000000000000..bd8e5a26d08bc
--- /dev/null
+++ b/client/views/omnichannel/filters/EditFilterPageContainer.js
@@ -0,0 +1,25 @@
+import { Callout } from '@rocket.chat/fuselage';
+import React from 'react';
+
+import PageSkeleton from '../../../components/PageSkeleton';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { AsyncStatePhase } from '../../../hooks/useAsyncState';
+import { useEndpointData } from '../../../hooks/useEndpointData';
+import EditFilterPage from './EditFilterPage';
+
+const EditFilterPageContainer = ({ id, onSave }) => {
+ const t = useTranslation();
+ const { value: data, phase: state } = useEndpointData(`livechat/filters/${id}`);
+
+ if (state === AsyncStatePhase.LOADING) {
+ return ;
+ }
+
+ if (state === AsyncStatePhase.REJECTED || !data?.filter) {
+ return {t('Error')}: error;
+ }
+
+ return ;
+};
+
+export default EditFilterPageContainer;
diff --git a/client/views/omnichannel/filters/FiltersForm.stories.js b/client/views/omnichannel/filters/FiltersForm.stories.js
new file mode 100644
index 0000000000000..6bbb524e2f454
--- /dev/null
+++ b/client/views/omnichannel/filters/FiltersForm.stories.js
@@ -0,0 +1,27 @@
+import { FieldGroup, Box } from '@rocket.chat/fuselage';
+import React from 'react';
+
+import { useForm } from '../../../hooks/useForm';
+import FiltersForm from './FiltersForm';
+
+export default {
+ title: 'omnichannel/FiltersForm',
+ component: FiltersForm,
+};
+
+export const Default = () => {
+ const { values, handlers } = useForm({
+ name: '',
+ description: '',
+ enabled: true,
+ regex: '',
+ slug: '',
+ });
+ return (
+
+
+ ;
+
+
+ );
+};
diff --git a/client/views/omnichannel/filters/FiltersForm.tsx b/client/views/omnichannel/filters/FiltersForm.tsx
new file mode 100644
index 0000000000000..3abc7a7d0df64
--- /dev/null
+++ b/client/views/omnichannel/filters/FiltersForm.tsx
@@ -0,0 +1,72 @@
+import { Box, Field, TextInput, ToggleSwitch } from '@rocket.chat/fuselage';
+import React, { ComponentProps, FC, FormEvent } from 'react';
+
+import { useTranslation } from '../../../contexts/TranslationContext';
+
+type FiltersFormProps = {
+ values: {
+ name: string;
+ description: string;
+ enabled: boolean;
+ regex: string;
+ slug: string;
+ };
+ handlers: {
+ handleName: (event: FormEvent) => void;
+ handleDescription: (event: FormEvent) => void;
+ handleEnabled: (event: FormEvent) => void;
+ handleRegex: (event: FormEvent) => void;
+ handleSlug: (event: FormEvent) => void;
+ };
+ className?: ComponentProps['className'];
+};
+
+const FiltersForm: FC = ({ values, handlers, className }) => {
+ const t = useTranslation();
+ const { name, description, enabled, regex, slug } = values;
+
+ const { handleName, handleDescription, handleEnabled, handleRegex, handleSlug } = handlers;
+
+ return (
+ <>
+
+
+ {t('Enabled')}
+
+
+
+
+
+
+ {t('Name')}
+
+
+
+
+
+ {t('Description')}
+
+
+
+
+
+ {t('Regex')}
+
+
+
+
+
+ {t('Slug')}
+
+
+
+
+ >
+ );
+};
+
+export default FiltersForm;
diff --git a/client/views/omnichannel/filters/FiltersPage.js b/client/views/omnichannel/filters/FiltersPage.js
new file mode 100644
index 0000000000000..0c47bb8666850
--- /dev/null
+++ b/client/views/omnichannel/filters/FiltersPage.js
@@ -0,0 +1,67 @@
+import { Button, Icon } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React, { useRef } from 'react';
+
+import NotAuthorizedPage from '../../../components/NotAuthorizedPage';
+import Page from '../../../components/Page';
+import VerticalBar from '../../../components/VerticalBar';
+import { usePermission } from '../../../contexts/AuthorizationContext';
+import { useRoute, useRouteParameter } from '../../../contexts/RouterContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import EditFilterPageContainer from './EditFilterPageContainer';
+import FiltersTableContainer from './FiltersTableContainer';
+import NewFilterPage from './NewFilterPage';
+
+const MonitorsPage = () => {
+ const t = useTranslation();
+
+ const canViewTriggers = usePermission('view-livechat-triggers');
+
+ const router = useRoute('omnichannel-filters');
+
+ const reload = useRef(() => {});
+
+ const context = useRouteParameter('context');
+ const id = useRouteParameter('id');
+
+ const handleAdd = useMutableCallback(() => {
+ router.push({ context: 'new' });
+ });
+
+ const handleCloseVerticalBar = useMutableCallback(() => {
+ router.push({});
+ });
+
+ if (!canViewTriggers) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {context && (
+
+
+ {t('Filter')}
+
+
+
+ {context === 'edit' && }
+ {context === 'new' && }
+
+
+ )}
+
+ );
+};
+
+export default MonitorsPage;
diff --git a/client/views/omnichannel/filters/FiltersRow.js b/client/views/omnichannel/filters/FiltersRow.js
new file mode 100644
index 0000000000000..5b2d56dfcb73c
--- /dev/null
+++ b/client/views/omnichannel/filters/FiltersRow.js
@@ -0,0 +1,83 @@
+import { Table, Icon, Button } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React, { memo } from 'react';
+
+import GenericModal from '../../../components/GenericModal';
+import { useSetModal } from '../../../contexts/ModalContext';
+import { useRoute } from '../../../contexts/RouterContext';
+import { useMethod } from '../../../contexts/ServerContext';
+import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+
+const FiltersRow = memo(function FiltersRow(props) {
+ const { _id, name, description, enabled, onDelete } = props;
+
+ const dispatchToastMessage = useToastMessageDispatch();
+ const t = useTranslation();
+
+ const setModal = useSetModal();
+
+ const bhRoute = useRoute('omnichannel-filters');
+
+ const deleteFilter = useMethod('livechat:removeFilter');
+
+ const handleClick = useMutableCallback(() => {
+ bhRoute.push({
+ context: 'edit',
+ id: _id,
+ });
+ });
+
+ const handleKeyDown = useMutableCallback((e) => {
+ if (!['Enter', 'Space'].includes(e.nativeEvent.code)) {
+ return;
+ }
+
+ handleClick();
+ });
+
+ const handleDelete = useMutableCallback((e) => {
+ e.stopPropagation();
+ const onDeleteFilter = async () => {
+ try {
+ await deleteFilter(_id);
+ dispatchToastMessage({ type: 'success', message: t('Filter_removed') });
+ onDelete();
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ setModal();
+ };
+
+ setModal(
+ setModal()}
+ confirmText={t('Delete')}
+ />,
+ );
+ });
+
+ return (
+
+ {name}
+ {description}
+ {enabled ? t('Yes') : t('No')}
+
+
+
+
+ );
+});
+
+export default FiltersRow;
diff --git a/client/views/omnichannel/filters/FiltersTable.js b/client/views/omnichannel/filters/FiltersTable.js
new file mode 100644
index 0000000000000..5d140d2052425
--- /dev/null
+++ b/client/views/omnichannel/filters/FiltersTable.js
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import GenericTable from '../../../components/GenericTable';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useResizeInlineBreakpoint } from '../../../hooks/useResizeInlineBreakpoint';
+import FiltersRow from './FiltersRow';
+
+export function FiltersTable({ filters, totalFilters, params, onChangeParams, onDelete }) {
+ const t = useTranslation();
+
+ const [ref, onMediumBreakpoint] = useResizeInlineBreakpoint([600], 200);
+
+ return (
+
+ {t('Name')}
+ {t('Description')}
+ {t('Enabled')}
+ {t('Remove')}
+ >
+ }
+ results={filters}
+ total={totalFilters}
+ params={params}
+ setParams={onChangeParams}
+ >
+ {(props) => (
+
+ )}
+
+ );
+}
+
+export default FiltersTable;
diff --git a/client/views/omnichannel/filters/FiltersTableContainer.js b/client/views/omnichannel/filters/FiltersTableContainer.js
new file mode 100644
index 0000000000000..4b036dde976f4
--- /dev/null
+++ b/client/views/omnichannel/filters/FiltersTableContainer.js
@@ -0,0 +1,41 @@
+import { Callout } from '@rocket.chat/fuselage';
+import React, { useState, useMemo } from 'react';
+
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { AsyncStatePhase } from '../../../hooks/useAsyncState';
+import { useEndpointData } from '../../../hooks/useEndpointData';
+import FiltersTable from './FiltersTable';
+
+const FiltersTableContainer = ({ reloadRef }) => {
+ const t = useTranslation();
+ const [params, setParams] = useState(() => ({ current: 0, itemsPerPage: 25 }));
+
+ const { current, itemsPerPage } = params;
+
+ const {
+ value: data,
+ phase: state,
+ reload,
+ } = useEndpointData(
+ 'livechat/filters',
+ useMemo(() => ({ offset: current, count: itemsPerPage }), [current, itemsPerPage]),
+ );
+
+ reloadRef.current = reload;
+
+ if (state === AsyncStatePhase.REJECTED) {
+ return {t('Error')}: error;
+ }
+
+ return (
+
+ );
+};
+
+export default FiltersTableContainer;
diff --git a/client/views/omnichannel/filters/NewFilterPage.js b/client/views/omnichannel/filters/NewFilterPage.js
new file mode 100644
index 0000000000000..b174ca015ebf2
--- /dev/null
+++ b/client/views/omnichannel/filters/NewFilterPage.js
@@ -0,0 +1,53 @@
+import { Button, FieldGroup, ButtonGroup } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import React from 'react';
+
+import { useRoute } from '../../../contexts/RouterContext';
+import { useMethod } from '../../../contexts/ServerContext';
+import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import { useForm } from '../../../hooks/useForm';
+import FiltersForm from './FiltersForm';
+
+const NewFilterPage = ({ onSave }) => {
+ const dispatchToastMessage = useToastMessageDispatch();
+ const t = useTranslation();
+
+ const router = useRoute('omnichannel-filters');
+
+ const save = useMethod('livechat:saveFilter');
+
+ const { values, handlers } = useForm({
+ name: '',
+ description: '',
+ enabled: true,
+ regex: '',
+ slug: '',
+ });
+
+ const handleSave = useMutableCallback(async () => {
+ try {
+ await save(values);
+ dispatchToastMessage({ type: 'success', message: t('Saved') });
+ onSave();
+ router.push({});
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export default NewFilterPage;
diff --git a/client/views/omnichannel/installation/Installation.js b/client/views/omnichannel/installation/Installation.js
index 186737caa76b2..72821f8ea4c47 100644
--- a/client/views/omnichannel/installation/Installation.js
+++ b/client/views/omnichannel/installation/Installation.js
@@ -16,7 +16,7 @@ const Installation = () => {
const installString = `