[NEW] Experimental Game Center (externalComponents implementation) (#15123)
parent
2c729b64ca
commit
292a7c3b30
@ -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), |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,69 @@ |
||||
<template name="GameCenter"> |
||||
{{#table onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}} |
||||
<tbody class="rc-game__list"> |
||||
<thead> |
||||
<tr> |
||||
<th class="js-sort rc-table-td--medium" data-sort="name"> |
||||
<div class="table-fake-th">{{_ "Name"}}</div> |
||||
</th> |
||||
<th class="rc-table-td"> |
||||
<div class="table-fake-th">{{_ "Description"}} </div> |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
{{#each games}} |
||||
<tr class="rc-game-center__game" data-name="{{name}}"> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
{{#if icon}} |
||||
<div class="rc-table-avatar" style="background-image:url({{icon}})"></div> |
||||
{{/if}} |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{name}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{#if summary}} |
||||
{{summary}} |
||||
{{else}} |
||||
{{description}} |
||||
{{/if}} |
||||
</span> |
||||
{{#if summary}} |
||||
<span class="rc-table-subtitle"> |
||||
{{description}} |
||||
</span> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<button class="rc-tooltip game-center__invite-players js-invite" aria-label="Invite Friends"> |
||||
{{> icon block="game-center__invite-players-icon" icon="plus"}} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
{{/each}} |
||||
{{#if isLoading}} |
||||
<tr> |
||||
<td colspan="3" style="position: relative;">{{> loading}}</td> |
||||
</tr> |
||||
{{/if}} |
||||
</tbody> |
||||
{{/table}} |
||||
<div class="rc-user-info-container flex-nav animated{{#unless showGame}} animated-hidden{{/unless}}"> |
||||
{{#if showGame}} |
||||
{{> GameContainer (gameContainerOptions) }} |
||||
{{/if}} |
||||
</div> |
||||
</template> |
||||
@ -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, |
||||
}); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,30 @@ |
||||
<template name="GameContainer"> |
||||
{{#if isContextualBar}} |
||||
<header class="contextual-bar__header"> |
||||
<div class="contextual-bar__header-data"> |
||||
{{#if showBackButton}} |
||||
<button |
||||
class="rc-button rc-button--nude contextual-bar__header-back-btn js-back" |
||||
title="{{_ 'Back_to_Game_Center'}}" |
||||
> |
||||
<i class="icon-angle-left"></i> |
||||
</button> |
||||
{{/if}} |
||||
<img class="rc-table-avatar contextual-bar__header-icon" src="{{ game.icon }}"> |
||||
<h1 class="contextual-bar__header-title">{{ game.name }}</h1> |
||||
</div> |
||||
<button class="contextual-bar__header-close js-close"> |
||||
{{> icon block="contextual-bar__header-close-icon" icon="plus"}} |
||||
</button> |
||||
</header> |
||||
{{/if}} |
||||
|
||||
{{#if isLoading}} |
||||
{{> loading}} |
||||
{{else}} |
||||
<div class="rc-game__container {{#if isModal}} rc-game-modal__container {{/if}}"> |
||||
<div class="rc-game__close">✖</div> |
||||
<iframe class="rc-game__main {{#if isModal}} rc-game-modal__main {{/if}}" src="{{ game.url }}"></iframe> |
||||
</div> |
||||
{{/if}} |
||||
</template> |
||||
@ -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, |
||||
}); |
||||
}); |
||||
@ -0,0 +1,54 @@ |
||||
<template name="InvitePlayers"> |
||||
|
||||
<section class="invite-players"> |
||||
<div class="invite-players__wrapper"> |
||||
<form id="invite-players" class="invite-players__content"> |
||||
<div class="invite-players__inputs"> |
||||
{{> SearchInvitePlayers |
||||
onClickTag=onClickTagUser |
||||
deleteLastItem=deleteLastItemUser |
||||
list=selectedUsers |
||||
onSelect=onSelectUser |
||||
collection='UserAndRoom' |
||||
endpoint='users.autocomplete' |
||||
field='username' |
||||
sort='username' |
||||
label="Invite_Users" |
||||
placeholder="Username_Placeholder" |
||||
name="users" |
||||
icon="at" |
||||
noMatchTemplate="userSearchEmpty" |
||||
templateItem="popupList_item_default" |
||||
modifier=userModifier |
||||
}} |
||||
</div> |
||||
<input form="invite-players" style="background-color: #1D74F5; float: right;" class="rc-button rc-button--right rc-button--primary js-invite-players" type="submit" data-button="create" value="Invite"> |
||||
</form> |
||||
</div> |
||||
</section> |
||||
</template> |
||||
|
||||
<template name="SearchInvitePlayers"> |
||||
<div class="rc-input" id='search-{{name}}' {{disabled}}> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ label}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
{{# if icon}} |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon=icon}} |
||||
</div> |
||||
{{/if}} |
||||
<div class="rc-tags{{# unless icon}} rc-tags--no-icon{{/unless}}"> |
||||
{{#each item in list}} {{> tag item}} {{/each}} |
||||
<input type="text" id="{{name}}" class="rc-tags__input" placeholder="{{_ placeholder}}" name="{{name}}" autocomplete="off" {{disabled}} /> |
||||
</div> |
||||
</div> |
||||
{{#with config}} |
||||
{{#if autocomplete 'isShowing'}} |
||||
{{> popupList data=config items=items ready=(autocomplete 'isLoaded')}} |
||||
{{/if}} |
||||
{{/with}} |
||||
</label> |
||||
<div class="rc-input__description">{{ description }}</div> |
||||
</div> |
||||
</template> |
||||
@ -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) => `<strong>${ part }</strong>`) }`; |
||||
}; |
||||
}, |
||||
userModifier() { |
||||
return (filter, text = '') => { |
||||
const f = filter.get(); |
||||
return `@${ f.length === 0 ? text : text.replace(new RegExp(filter.get()), (part) => `<strong>${ part }</strong>`) }`; |
||||
}; |
||||
}, |
||||
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; |
||||
}); |
||||
@ -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, |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,3 +1,8 @@ |
||||
import './routes'; |
||||
|
||||
import './gameCenter/tabBar'; |
||||
import './gameCenter/gameContainer'; |
||||
import './gameCenter/gameCenter'; |
||||
import './gameCenter/invitePlayers'; |
||||
|
||||
export { Apps } from './orchestrator'; |
||||
|
||||
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 206 KiB |
Loading…
Reference in new issue