From 292a7c3b30e1946f65680c62a41b6771a6acfabb Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Thu, 19 Mar 2020 19:16:42 -0300 Subject: [PATCH] [NEW] Experimental Game Center (externalComponents implementation) (#15123) --- app/api/server/v1/groups.js | 2 +- app/apps/assets/stylesheets/apps.css | 51 +++ app/apps/client/RealAppsEngineUIHost.js | 58 +++ app/apps/client/gameCenter/gameCenter.html | 69 ++++ app/apps/client/gameCenter/gameCenter.js | 110 +++++ app/apps/client/gameCenter/gameContainer.html | 30 ++ app/apps/client/gameCenter/gameContainer.js | 65 +++ app/apps/client/gameCenter/invitePlayers.html | 54 +++ app/apps/client/gameCenter/invitePlayers.js | 219 ++++++++++ app/apps/client/gameCenter/tabBar.js | 35 ++ app/apps/client/index.js | 5 + app/apps/client/orchestrator.js | 11 +- app/apps/server/bridges/listeners.js | 6 + app/apps/server/communication/rest.js | 26 ++ app/apps/server/orchestrator.js | 16 + app/ui-flextab/client/flexTabBar.js | 3 + app/ui-master/public/README.md | 9 +- app/ui-master/public/icons.svg | 377 ++++++++++++++++++ app/ui-master/public/icons/game.svg | 3 + .../icons/{livechat.svg => omnichannel.svg} | 4 +- app/ui-utils/client/index.js | 2 +- app/ui-utils/client/lib/TabBar.js | 4 +- package-lock.json | 11 +- package.json | 2 +- packages/rocketchat-i18n/i18n/en.i18n.json | 5 + packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 3 + packages/rocketchat-i18n/i18n/zh.i18n.json | 5 +- private/public/icons.svg | 9 +- 28 files changed, 1167 insertions(+), 27 deletions(-) create mode 100644 app/apps/client/RealAppsEngineUIHost.js create mode 100644 app/apps/client/gameCenter/gameCenter.html create mode 100644 app/apps/client/gameCenter/gameCenter.js create mode 100644 app/apps/client/gameCenter/gameContainer.html create mode 100644 app/apps/client/gameCenter/gameContainer.js create mode 100644 app/apps/client/gameCenter/invitePlayers.html create mode 100644 app/apps/client/gameCenter/invitePlayers.js create mode 100644 app/apps/client/gameCenter/tabBar.js create mode 100644 app/ui-master/public/icons.svg create mode 100644 app/ui-master/public/icons/game.svg rename app/ui-master/public/icons/{livechat.svg => omnichannel.svg} (96%) diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index 6c2b45b388b..254c5b88534 100644 --- a/app/api/server/v1/groups.js +++ b/app/api/server/v1/groups.js @@ -8,7 +8,7 @@ import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMes import { API } from '../api'; // Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { +export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { throw new Meteor.Error('error-room-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } diff --git a/app/apps/assets/stylesheets/apps.css b/app/apps/assets/stylesheets/apps.css index 438b99d813e..e9776bd9a83 100644 --- a/app/apps/assets/stylesheets/apps.css +++ b/app/apps/assets/stylesheets/apps.css @@ -370,6 +370,57 @@ } } +.rc-game { + &__list { + .rc-table-title { + width: 135px; + } + } + + &__container { + height: calc(100% - 79px); + } + + &__close { + position: absolute; + top: -30px; + right: -25px; + + cursor: pointer; + + color: #e9ebee; + + font-size: 20px; + + &:hover { + color: white; + } + } + + &__main { + min-width: 400px; + height: 100%; + } +} + +.rc-game-modal { + &__container { + margin: -14px -14px -18px; + } + + &__main { + min-height: 730px; + } +} + +.rc-icon.game-center__invite-players-icon.game-center__invite-players-icon--plus { + font-size: 18px; + + &:hover { + color: #1d74f5; + } +} + @keyframes play90 { 0% { right: -798px; diff --git a/app/apps/client/RealAppsEngineUIHost.js b/app/apps/client/RealAppsEngineUIHost.js new file mode 100644 index 00000000000..5d5274b7afb --- /dev/null +++ b/app/apps/client/RealAppsEngineUIHost.js @@ -0,0 +1,58 @@ +import { Meteor } from 'meteor/meteor'; +import { Session } from 'meteor/session'; +import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHost'; + +import { Rooms } from '../../models/client'; +import { APIClient } from '../../utils/client'; +import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; + +export class RealAppsEngineUIHost extends AppsEngineUIHost { + constructor() { + super(); + + this._baseURL = document.baseURI.slice(0, -1); + } + + getUserAvatarUrl(username) { + const avatarUrl = getUserAvatarURL(username); + + if (!avatarUrl.startsWith('http') && !avatarUrl.startsWith('data')) { + return `${ this._baseURL }${ avatarUrl }`; + } + + return avatarUrl; + } + + async getClientRoomInfo() { + const { name: slugifiedName, _id: id } = Rooms.findOne(Session.get('openedRoom')); + + let cachedMembers = []; + try { + const { members } = await APIClient.get('v1/groups.members', { roomId: id }); + + cachedMembers = members.map(({ _id, username }) => ({ + id: _id, + username, + avatarUrl: this.getUserAvatarUrl(username), + })); + } catch (error) { + console.warn(error); + } + + return { + id, + slugifiedName, + members: cachedMembers, + }; + } + + async getClientUserInfo() { + const { username, _id } = Meteor.user(); + + return { + id: _id, + username, + avatarUrl: this.getUserAvatarUrl(username), + }; + } +} diff --git a/app/apps/client/gameCenter/gameCenter.html b/app/apps/client/gameCenter/gameCenter.html new file mode 100644 index 00000000000..2ca752b93ca --- /dev/null +++ b/app/apps/client/gameCenter/gameCenter.html @@ -0,0 +1,69 @@ + diff --git a/app/apps/client/gameCenter/gameCenter.js b/app/apps/client/gameCenter/gameCenter.js new file mode 100644 index 00000000000..91e092b4f97 --- /dev/null +++ b/app/apps/client/gameCenter/gameCenter.js @@ -0,0 +1,110 @@ +import toastr from 'toastr'; +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; + +import { modal } from '../../../ui-utils/client'; +import { APIClient, t } from '../../../utils/client'; + +const getExternalComponents = async (instance) => { + try { + const { externalComponents } = await APIClient.get('apps/externalComponents'); + instance.games.set(externalComponents); + } catch (e) { + toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message); + } + + instance.isLoading.set(false); + instance.ready.set(true); +}; + +const openGame = (gameManifestInfo) => { + const instance = Template.instance(); + const { location = 'MODAL' } = gameManifestInfo; + + if (location === 'CONTEXTUAL_BAR') { + instance.gameManifestInfo.set(gameManifestInfo); + } else if (location === 'MODAL') { + modal.open({ + allowOutsideClick: false, + data: { + game: gameManifestInfo, + }, + template: 'GameContainer', + type: 'rc-game', + }); + } +}; + +Template.GameCenter.helpers({ + isReady() { + if (Template.instance().ready != null) { + return Template.instance().ready.get(); + } + return false; + }, + games() { + return Template.instance().games.get(); + }, + isLoading() { + return Template.instance().isLoading.get(); + }, + onTableScroll() { + const instance = Template.instance(); + if (instance.loading || instance.end.get()) { + return; + } + return function(currentTarget) { + if (currentTarget.offsetHeight + currentTarget.scrollTop >= currentTarget.scrollHeight - 100) { + return instance.page.set(instance.page.get() + 1); + } + }; + }, + showGame() { + return Template.instance().gameManifestInfo.get(); + }, + gameContainerOptions() { + const { gameManifestInfo, clearGameManifestInfo } = Template.instance(); + + return { + game: gameManifestInfo.get(), + showBackButton: true, + clearGameManifestInfo, + }; + }, +}); + +Template.GameCenter.onCreated(function() { + this.ready = new ReactiveVar(false); + this.games = new ReactiveVar([]); + this.isLoading = new ReactiveVar(true); + this.page = new ReactiveVar(0); + this.end = new ReactiveVar(false); + + this.gameManifestInfo = new ReactiveVar(null); + + this.clearGameManifestInfo = () => { + this.gameManifestInfo.set(null); + }; + + getExternalComponents(this); +}); + +Template.GameCenter.events({ + 'click .rc-game-center__game'() { + const gameManifestInfo = this; + + openGame(gameManifestInfo); + }, + 'click .js-invite'(event) { + event.stopPropagation(); + modal.open({ + title: t('Invite You Friends to Join'), + content: 'InvitePlayers', + data: this, + confirmOnEnter: false, + showCancelButton: false, + showConfirmButton: false, + html: false, + }); + }, +}); diff --git a/app/apps/client/gameCenter/gameContainer.html b/app/apps/client/gameCenter/gameContainer.html new file mode 100644 index 00000000000..6bbb136df0e --- /dev/null +++ b/app/apps/client/gameCenter/gameContainer.html @@ -0,0 +1,30 @@ + diff --git a/app/apps/client/gameCenter/gameContainer.js b/app/apps/client/gameCenter/gameContainer.js new file mode 100644 index 00000000000..2d26d4e541c --- /dev/null +++ b/app/apps/client/gameCenter/gameContainer.js @@ -0,0 +1,65 @@ +import { Template } from 'meteor/templating'; + +import { modal } from '../../../ui-utils/client'; +import { Apps } from '../orchestrator'; +import { APIClient } from '../../../utils/client'; + +import './gameContainer.html'; + +const getExternalComponent = async () => { + const { data: { game: externalComponent } } = Template.instance(); + const realAppClientUIHost = Apps.getUIHost(); + const currentUser = await realAppClientUIHost.getClientUserInfo(); + const currentRoom = await realAppClientUIHost.getClientRoomInfo(); + + externalComponent.state = { + currentUser, + currentRoom, + }; + + return externalComponent; +}; + +Template.GameContainer.helpers({ + isContextualBar() { + const { data: { game } } = Template.instance(); + const { location } = game; + + return location === 'CONTEXTUAL_BAR'; + }, + isModal() { + const { data: { game } } = Template.instance(); + const { location } = game; + + return location === 'MODAL'; + }, +}); + +Template.GameContainer.events({ + 'click .rc-game__close'() { + modal.cancel(); + }, + 'click .js-back'() { + const { data: { clearGameManifestInfo } } = Template.instance(); + + clearGameManifestInfo(); + }, +}); + +Template.GameContainer.onCreated(async () => { + const externalComponent = await getExternalComponent(); + + APIClient.post('apps/externalComponentEvent', { + event: 'IPostExternalComponentOpened', + externalComponent, + }); +}); + +Template.GameContainer.onDestroyed(async () => { + const externalComponent = await getExternalComponent(); + + APIClient.post('apps/externalComponentEvent', { + event: 'IPostExternalComponentClosed', + externalComponent, + }); +}); diff --git a/app/apps/client/gameCenter/invitePlayers.html b/app/apps/client/gameCenter/invitePlayers.html new file mode 100644 index 00000000000..a74e14ce30b --- /dev/null +++ b/app/apps/client/gameCenter/invitePlayers.html @@ -0,0 +1,54 @@ + + + diff --git a/app/apps/client/gameCenter/invitePlayers.js b/app/apps/client/gameCenter/invitePlayers.js new file mode 100644 index 00000000000..31616bbf386 --- /dev/null +++ b/app/apps/client/gameCenter/invitePlayers.js @@ -0,0 +1,219 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Blaze } from 'meteor/blaze'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { AutoComplete } from '../../../meteor-autocomplete/client'; +import { roomTypes } from '../../../utils/client'; +import { ChatRoom } from '../../../models/client'; +import { call, modal } from '../../../ui-utils/client'; + +import './invitePlayers.html'; + +Template.InvitePlayers.helpers({ + onSelectUser() { + return Template.instance().onSelectUser; + }, + selectedUsers() { + const myUsername = Meteor.user().username; + const { message } = this; + const users = Template.instance().selectedUsers.get().map((e) => e); + if (message) { + users.unshift(message.u); + } + return users.filter(({ username }) => myUsername !== username); + }, + onClickTagUser() { + return Template.instance().onClickTagUser; + }, + deleteLastItemUser() { + return Template.instance().deleteLastItemUser; + }, + onClickTagRoom() { + return Template.instance().onClickTagRoom; + }, + deleteLastItemRoom() { + return Template.instance().deleteLastItemRoom; + }, + selectedRoom() { + return Template.instance().selectedRoom.get(); + }, + onSelectRoom() { + return Template.instance().onSelectRoom; + }, + roomCollection() { + return ChatRoom; + }, + roomSelector() { + return (expression) => ({ name: { $regex: `.*${ expression }.*` } }); + }, + roomModifier() { + return (filter, text = '') => { + const f = filter.get(); + return `#${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `${ part }`) }`; + }; + }, + userModifier() { + return (filter, text = '') => { + const f = filter.get(); + return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `${ part }`) }`; + }; + }, + nameSuggestion() { + return Template.instance().discussionName.get(); + }, +}); + +Template.InvitePlayers.events({ + async 'submit #invite-players, click .js-invite-players'(e, instance) { + e.preventDefault(); + + const { data: { name } } = instance; + const users = instance.selectedUsers.get().map(({ username }) => username); + const privateGroupName = `${ name.replace(/\s/g, '-') }-${ Random.id(10) }`; + + try { + const result = await call('createPrivateGroup', privateGroupName, users); + + roomTypes.openRouteLink(result.t, result); + + // setTimeout ensures the message is only sent after the + // user has been redirected to the new room, preventing a + // weird bug that made the message appear as unsent until + // the screen gets refreshed + setTimeout(() => call('sendMessage', { + _id: Random.id(), + rid: result.rid, + msg: TAPi18n.__('Game_Center_Play_Game_Together', { name }), + }), 100); + + modal.close(); + } catch (err) { + console.warn(err); + } + }, +}); + +Template.InvitePlayers.onCreated(function() { + this.selectedUsers = new ReactiveVar([]); + this.onSelectUser = ({ item: user }) => { + if (user.username === Meteor.user().username) { + return; + } + const users = this.selectedUsers.get(); + if (!users.find((u) => user.username === u.username)) { + this.selectedUsers.set([...users, user]); + } + }; + this.onClickTagUser = ({ username }) => { + this.selectedUsers.set(this.selectedUsers.get().filter((user) => user.username !== username)); + }; + this.deleteLastItemUser = () => { + const arr = this.selectedUsers.get(); + arr.pop(); + this.selectedUsers.set(arr); + }; +}); + +Template.SearchInvitePlayers.helpers({ + list() { + return this.list; + }, + items() { + return Template.instance().ac.filteredList(); + }, + config() { + const { filter } = Template.instance(); + const { noMatchTemplate, templateItem, modifier } = Template.instance().data; + return { + filter: filter.get(), + template_item: templateItem, + noMatchTemplate, + modifier(text) { + return modifier(filter, text); + }, + }; + }, + autocomplete(key) { + const instance = Template.instance(); + const param = instance.ac[key]; + return typeof param === 'function' ? param.apply(instance.ac) : param; + }, +}); + +Template.SearchInvitePlayers.events({ + 'input input'(e, t) { + const input = e.target; + const position = input.selectionEnd || input.selectionStart; + const { length } = input.value; + document.activeElement === input && e && /input/i.test(e.type) && (input.selectionEnd = position + input.value.length - length); + t.filter.set(input.value); + }, + 'click .rc-popup-list__item'(e, t) { + t.ac.onItemClick(this, e); + }, + 'keydown input'(e, t) { + const KEYCODE_BACKSPACE = 8; + const KEYCODE_DELETE = 46; + + t.ac.onKeyDown(e); + if ([KEYCODE_BACKSPACE, KEYCODE_DELETE].includes(e.keyCode) && e.target.value === '') { + const { deleteLastItem } = t; + return deleteLastItem && deleteLastItem(); + } + }, + 'keyup input'(e, t) { + t.ac.onKeyUp(e); + }, + 'focus input'(e, t) { + t.ac.onFocus(e); + }, + 'blur input'(e, t) { + t.ac.onBlur(e); + }, + 'click .rc-tags__tag'({ target }, t) { + const { onClickTag } = t; + return onClickTag & onClickTag(Blaze.getData(target)); + }, +}); +Template.SearchInvitePlayers.onRendered(function() { + const { name } = this.data; + + this.ac.element = this.firstNode.querySelector(`[name=${ name }]`); + this.ac.$element = $(this.ac.element); +}); + +Template.SearchInvitePlayers.onCreated(function() { + this.filter = new ReactiveVar(''); + this.selected = new ReactiveVar([]); + this.onClickTag = this.data.onClickTag; + this.deleteLastItem = this.data.deleteLastItem; + + const { collection, endpoint, field, sort, onSelect, selector = (match) => ({ term: match }) } = this.data; + this.ac = new AutoComplete( + { + selector: { + anchor: '.rc-input__label', + item: '.rc-popup-list__item', + container: '.rc-popup-list__list', + }, + onSelect, + position: 'fixed', + limit: 10, + inputDelay: 300, + rules: [ + { + collection, + endpoint, + field, + matchAll: true, + doNotChangeWidth: false, + selector, + sort, + }, + ], + }); + this.ac.tmplInst = this; +}); diff --git a/app/apps/client/gameCenter/tabBar.js b/app/apps/client/gameCenter/tabBar.js new file mode 100644 index 00000000000..0d5aff24c95 --- /dev/null +++ b/app/apps/client/gameCenter/tabBar.js @@ -0,0 +1,35 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + +import { APIClient } from '../../../utils/client'; +import { TabBar, TABBAR_DEFAULT_VISIBLE_ICON_COUNT } from '../../../ui-utils/client'; +import { settings } from '../../../settings/client'; + +import './gameCenter.html'; + +Meteor.startup(function() { + Tracker.autorun(async function() { + if (!settings.get('Apps_Game_Center_enabled')) { + TabBar.size = TABBAR_DEFAULT_VISIBLE_ICON_COUNT; + return TabBar.removeButton('gameCenter'); + } + + const { externalComponents } = await APIClient.get('apps/externalComponents'); + + if (!externalComponents.length) { + TabBar.size = TABBAR_DEFAULT_VISIBLE_ICON_COUNT; + return TabBar.removeButton('gameCenter'); + } + + TabBar.size = TABBAR_DEFAULT_VISIBLE_ICON_COUNT + 1; + + TabBar.addButton({ + groups: ['channel', 'group', 'direct'], + id: 'gameCenter', + i18nTitle: 'Game_Center', + icon: 'game', + template: 'GameCenter', + order: -1, + }); + }); +}); diff --git a/app/apps/client/index.js b/app/apps/client/index.js index a89134a70f5..1810371eda2 100644 --- a/app/apps/client/index.js +++ b/app/apps/client/index.js @@ -1,3 +1,8 @@ import './routes'; +import './gameCenter/tabBar'; +import './gameCenter/gameContainer'; +import './gameCenter/gameCenter'; +import './gameCenter/invitePlayers'; + export { Apps } from './orchestrator'; diff --git a/app/apps/client/orchestrator.js b/app/apps/client/orchestrator.js index f7b44912401..2252c886ce9 100644 --- a/app/apps/client/orchestrator.js +++ b/app/apps/client/orchestrator.js @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; import toastr from 'toastr'; import { AppWebsocketReceiver } from './communication'; @@ -7,6 +8,7 @@ import { AdminBox } from '../../ui-utils'; import { CachedCollectionManager } from '../../ui-cached-collection'; import { hasAtLeastOnePermission } from '../../authorization'; import { handleI18nResources } from './i18n'; +import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; const createDeferredValue = () => { let resolve; @@ -21,6 +23,8 @@ const createDeferredValue = () => { class AppClientOrchestrator { constructor() { + this._appClientUIHost = new RealAppsEngineUIHost(); + this._manager = new AppClientManager(this._appClientUIHost); this.isLoaded = false; [this.deferredIsEnabled, this.setEnabled] = createDeferredValue(); } @@ -42,7 +46,9 @@ class AppClientOrchestrator { this.setEnabled(isEnabled); } - getWsListener = () => this.ws + getWsListener = () => this.ws; + + getAppClientManager = () => this._manager; registerAdminMenuItems = () => { AdminBox.addOption({ @@ -179,6 +185,8 @@ class AppClientOrchestrator { const categories = await APIClient.get('apps', { categories: 'true' }); return categories; } + + getUIHost = () => this._appClientUIHost; } export const Apps = new AppClientOrchestrator(); @@ -191,6 +199,7 @@ Meteor.startup(() => { return; } + Apps.getAppClientManager().initialize(); Apps.load(isEnabled); }); }); diff --git a/app/apps/server/bridges/listeners.js b/app/apps/server/bridges/listeners.js index 88bdc7c499c..693c55271c3 100644 --- a/app/apps/server/bridges/listeners.js +++ b/app/apps/server/bridges/listeners.js @@ -37,6 +37,12 @@ export class AppListenerBridge { // } } + async externalComponentEvent(inte, externalComponent) { + const result = await this.orch.getManager().getListenerManager().executeListener(inte, externalComponent); + + return result; + } + async uiKitInteractionEvent(inte, action) { return this.orch.getManager().getListenerManager().executeListener(inte, action); diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index ccfd34fdf84..b5bdf523c21 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -257,6 +257,14 @@ export class AppsRestApi { }, }); + this.api.addRoute('externalComponents', { authRequired: false }, { + get() { + const externalComponents = orchestrator.getProvidedComponents(); + + return API.v1.success({ externalComponents }); + }, + }); + this.api.addRoute('languages', { authRequired: false }, { get() { const apps = manager.get().map((prl) => ({ @@ -268,6 +276,24 @@ export class AppsRestApi { }, }); + this.api.addRoute('externalComponentEvent', { authRequired: true }, { + post() { + if (!this.bodyParams.externalComponent || !['IPostExternalComponentOpened', 'IPostExternalComponentClosed'].includes(this.bodyParams.event)) { + return API.v1.failure({ error: 'Event and externalComponent must be provided.' }); + } + + try { + const { event, externalComponent } = this.bodyParams; + const result = Apps.getBridges().getListenerBridge().externalComponentEvent(event, externalComponent); + + return API.v1.success({ result }); + } catch (e) { + orchestrator.getRocketChatLogger().error(`Error triggering external components' events ${ e.response.data }`); + return API.v1.internalError(); + } + }, + }); + this.api.addRoute('bundles/:id/apps', { authRequired: true, permissionsRequired: ['manage-apps'] }, { get() { const baseUrl = orchestrator.getMarketplaceUrl(); diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js index 1cd352251e6..502ea2537a7 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -88,6 +88,10 @@ class AppServerOrchestrator { return this._manager; } + getProvidedComponents() { + return this._manager.getExternalComponentManager().getProvidedComponents(); + } + isInitialized() { return this._isInitialized; } @@ -171,9 +175,21 @@ settings.addGroup('General', function() { public: true, hidden: false, }); + + this.add('Apps_Game_Center_enabled', false, { + type: 'boolean', + enableQuery: { + _id: 'Apps_Framework_enabled', + value: true, + }, + hidden: false, + public: true, + alert: 'Experimental_Feature_Alert', + }); }); }); + settings.get('Apps_Framework_enabled', (key, isEnabled) => { // In case this gets called before `Meteor.startup` if (!Apps.isInitialized()) { diff --git a/app/ui-flextab/client/flexTabBar.js b/app/ui-flextab/client/flexTabBar.js index 59160c93d15..b1daa65d229 100644 --- a/app/ui-flextab/client/flexTabBar.js +++ b/app/ui-flextab/client/flexTabBar.js @@ -59,6 +59,9 @@ const filterButtons = (button, anonymous, rid) => { if (button.id === 'thread' && !settings.get('Threads_enabled')) { return false; } + if (button.id === 'gameCenter' && !settings.get('Apps_Game_Center_enabled')) { + return false; + } return true; }; Template.flexTabBar.helpers({ diff --git a/app/ui-master/public/README.md b/app/ui-master/public/README.md index 660c3f89df2..4455152ef41 100644 --- a/app/ui-master/public/README.md +++ b/app/ui-master/public/README.md @@ -6,12 +6,5 @@ run below commands inside `app/ui-master/public` directory ```js node generateSprite.js -node generateHTML.js +cp icons.svg ../../../private/public ``` - - -After that 2 new files named `icons.html` and `icons.svg` will be generated. You need to cut and replace these two files to following locations. - -- icons.html to `public/public/icons.html` - -- icons.svg to `private/public/icons.svg` diff --git a/app/ui-master/public/icons.svg b/app/ui-master/public/icons.svg new file mode 100644 index 00000000000..0d2fbfeae59 --- /dev/null +++ b/app/ui-master/public/icons.svg @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/ui-master/public/icons/game.svg b/app/ui-master/public/icons/game.svg new file mode 100644 index 00000000000..2fd484da958 --- /dev/null +++ b/app/ui-master/public/icons/game.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/ui-master/public/icons/livechat.svg b/app/ui-master/public/icons/omnichannel.svg similarity index 96% rename from app/ui-master/public/icons/livechat.svg rename to app/ui-master/public/icons/omnichannel.svg index 18be34ba8ac..5c7ba65cb5e 100644 --- a/app/ui-master/public/icons/livechat.svg +++ b/app/ui-master/public/icons/omnichannel.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/app/ui-utils/client/index.js b/app/ui-utils/client/index.js index 293698e9d66..813392fcf96 100644 --- a/app/ui-utils/client/index.js +++ b/app/ui-utils/client/index.js @@ -17,7 +17,7 @@ export { Layout } from './lib/Layout'; export { IframeLogin, iframeLogin } from './lib/IframeLogin'; export { fireGlobalEvent } from './lib/fireGlobalEvent'; export { getAvatarAsPng, updateAvatarOfUsername } from './lib/avatar'; -export { TabBar } from './lib/TabBar'; +export { TabBar, TABBAR_DEFAULT_VISIBLE_ICON_COUNT } from './lib/TabBar'; export { RocketChatTabBar } from './lib/RocketChatTabBar'; export { popout } from './lib/popout'; export { messageProperties } from '../lib/MessageProperties'; diff --git a/app/ui-utils/client/lib/TabBar.js b/app/ui-utils/client/lib/TabBar.js index 761b965ea44..b9ec9039e3d 100644 --- a/app/ui-utils/client/lib/TabBar.js +++ b/app/ui-utils/client/lib/TabBar.js @@ -1,6 +1,8 @@ import _ from 'underscore'; import { ReactiveVar } from 'meteor/reactive-var'; +export const TABBAR_DEFAULT_VISIBLE_ICON_COUNT = 4; + export const TabBar = new class TabBar { get size() { return this._size.get(); @@ -12,7 +14,7 @@ export const TabBar = new class TabBar { constructor() { this.buttons = new ReactiveVar({}); - this._size = new ReactiveVar(4); + this._size = new ReactiveVar(TABBAR_DEFAULT_VISIBLE_ICON_COUNT); this.extraGroups = {}; } diff --git a/package-lock.json b/package-lock.json index 8c97b0aaed9..0296bebbb17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2681,9 +2681,9 @@ } }, "@rocket.chat/apps-engine": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.12.0.tgz", - "integrity": "sha512-HqhUQIisnMdx9X/ZnQh71dyxL+NLKtMjCyf7qoPmKs/MzMJ2a/Ix4AjSL5SRajM3SGlB1+cZ5/+vVZNeXOe68g==", + "version": "1.13.0-beta.2857", + "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.13.0-beta.2857.tgz", + "integrity": "sha512-zWOmvGv1p1sj7s4aJAoPWIJ9Mmuz7Hlq3BKQYNh8ELjM1IiRXd1dwuyl+5q1hBuru+QZ7T7QZ+qCOa2OcM8tfQ==", "requires": { "adm-zip": "^0.4.9", "cryptiles": "^4.1.3", @@ -2694,11 +2694,6 @@ "uuid": "^3.2.1" }, "dependencies": { - "adm-zip": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz", - "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==" - }, "typescript": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", diff --git a/package.json b/package.json index 15dced2347e..4d0bb6be601 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "@google-cloud/language": "^3.7.0", "@google-cloud/storage": "^2.3.1", "@google-cloud/vision": "^1.8.0", - "@rocket.chat/apps-engine": "^1.12.0", + "@rocket.chat/apps-engine": "^1.13.0-beta.2857", "@rocket.chat/fuselage": "^0.3.0", "@rocket.chat/fuselage-hooks": "^0.3.0", "@rocket.chat/fuselage-ui-kit": "^0.3.0", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index e08e476ce93..66396ccf61b 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -385,6 +385,7 @@ "Apps_Framework_Development_Mode": "Enable development mode", "Apps_Framework_Development_Mode_Description": "Development mode allows the installation of Apps that are not from the Rocket.Chat's Marketplace.", "Apps_Framework_enabled": "Enable the App Framework", + "Apps_Game_Center_enabled": "Enable the Game Center", "Apps_Marketplace_Deactivate_App_Prompt": "Do you really want to disable this app?", "Apps_Marketplace_Modify_App_Subscription": "Modify Subscription", "Apps_Marketplace_Uninstall_App_Prompt": "Do you really want to uninstall this app?", @@ -487,6 +488,7 @@ "Back": "Back", "Back_to_applications": "Back to applications", "Back_to_chat": "Back to chat", + "Back_to_Game_Center": "Back to Game Center", "Back_to_imports": "Back to imports", "Back_to_integration_detail": "Back to the integration detail", "Back_to_integrations": "Back to integrations", @@ -1436,6 +1438,7 @@ "except_pinned": "(except those that are pinned)", "Execute_Synchronization_Now": "Execute Synchronization Now", "Exit_Full_Screen": "Exit Full Screen", + "Experimental_Feature_Alert": "This is an experimental feature! Please be aware that it may change, break, or even be removed in the future without any notice.", "Expiration": "Expiration", "Expiration_(Days)": "Expiration (Days)", "Export_My_Data": "Export My Data (JSON)", @@ -1595,6 +1598,8 @@ "From_Email": "From Email", "From_email_warning": "Warning: The field From is subject to your mail server settings.", "Full_Screen": "Full Screen", + "Game_Center": "Game Center", + "Game_Center_Play_Game_Together": "@here Let's play __name__ together!", "Gaming": "Gaming", "General": "General", "Get_link": "Get Link", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 7911dd0ea69..ecf78a99815 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -370,6 +370,7 @@ "Apps_Engine_Version": "Versão da Apps Engine", "Apps_Framework_Development_Mode": "Habilitar modo de desenvolvimento", "Apps_Framework_enabled": "Ativar o App Framework", + "Apps_Game_Center_enabled": "Habilitar Game Center", "Apps_Settings": "Configurações da aplicação", "Apps_WhatIsIt": "Apps: o que são eles?", "Apps_WhatIsIt_paragraph1": "Um novo ícone na área de administração! O que significa e quais são as aplicações?", @@ -1335,6 +1336,7 @@ "except_pinned": "(exceto aqueles que estão presos)", "Execute_Synchronization_Now": "Execute sincronização agora", "Exit_Full_Screen": "Sair da tela cheia", + "Experimental_Feature_Alert": "Esta é uma funcionalidade experimental! Essas funcionalidades podem sofrer mudanças, parar de funcionar, ou até serem removidas sem aviso prévio.", "Expiration": "Validade", "Expiration_(Days)": "Validade (Dias)", "Export_My_Data": "Exportar meus dados (JSON)", @@ -1472,6 +1474,7 @@ "From_Email": "Email De", "From_email_warning": "Aviso: O campo De está sujeito às configurações do seu servidor de emails.", "Full_Screen": "Tela cheia", + "Game_Center": "Game Center", "Gaming": "Jogos", "General": "Geral", "Get_link": "Obter Link", diff --git a/packages/rocketchat-i18n/i18n/zh.i18n.json b/packages/rocketchat-i18n/i18n/zh.i18n.json index 4fc64ad8338..735d4d3d151 100644 --- a/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -374,6 +374,7 @@ "Apps_Framework_Development_Mode": "启用开发模式", "Apps_Framework_Development_Mode_Description": "开发模式允许您安装那些不在 Rocket.Chat 市场中的应用。", "Apps_Framework_enabled": "启用应用框架", + "Apps_Game_Center_enabled": "启用游戏中心", "Apps_Marketplace_Deactivate_App_Prompt": "是否确定要禁用此应用程序?", "Apps_Marketplace_Modify_App_Subscription": "修改订阅", "Apps_Marketplace_Uninstall_App_Prompt": "是否确定要卸载此应用程序?", @@ -1401,6 +1402,7 @@ "except_pinned": "(固定的除外)", "Execute_Synchronization_Now": "立即执行同步", "Exit_Full_Screen": "退出全屏", + "Experimental_Feature_Alert": "这是一个实验性特性!请注意这个特性随时都会改变、损坏甚至在未来版本中不加任何通知地被移除。", "Expiration": "期满", "Expiration_(Days)": "到期天数", "Export_My_Data": "导出我的数据", @@ -1549,6 +1551,7 @@ "From_Email": "从电子邮件", "From_email_warning": "警告From 字段来自于邮件服务器的设置。", "Full_Screen": "全屏", + "Game_Center": "游戏中心", "Gaming": "游戏", "General": "通用", "Get_link": "获取链接", @@ -3547,4 +3550,4 @@ "Your_server_link": "您的服务器链接", "Your_temporary_password_is_password": "您的暂时密码为 [password]。", "Your_workspace_is_ready": "您的工作区已准备好使用🎉" -} \ No newline at end of file +} diff --git a/private/public/icons.svg b/private/public/icons.svg index 86aae757dca..0d2fbfeae59 100644 --- a/private/public/icons.svg +++ b/private/public/icons.svg @@ -146,6 +146,9 @@ + + + @@ -197,9 +200,6 @@ - - - @@ -236,6 +236,9 @@ + + +