diff options
author | sualko <klaus@jsxc.org> | 2018-09-05 12:05:56 +0300 |
---|---|---|
committer | sualko <klaus@jsxc.org> | 2018-10-17 14:03:54 +0300 |
commit | 8d04c91ee21ba45c51b5e7391f5182f214e196ee (patch) | |
tree | c695650189fa936295861544dfe0d79991b4ffd0 /ts | |
parent | e5214aa68c39c0ebeb53be2a3b1c8cdb2797b7eb (diff) |
feat: migrate to jsxc v4.0.0-alpha
- use webpack to build packages
- use typescript
Diffstat (limited to 'ts')
-rw-r--r-- | ts/Bootstrap.ts | 33 | ||||
-rw-r--r-- | ts/CONST.ts | 7 | ||||
-rw-r--r-- | ts/ChatIconInjector.ts | 11 | ||||
-rw-r--r-- | ts/ChatSubmitButtonInjector.ts | 41 | ||||
-rw-r--r-- | ts/ContactMenuObserver.todo | 90 | ||||
-rw-r--r-- | ts/DefaultAvatar.ts | 84 | ||||
-rw-r--r-- | ts/FormWatcher.ts | 55 | ||||
-rw-r--r-- | ts/InternalBackend.todo | 64 | ||||
-rw-r--r-- | ts/SettingsLoader.ts | 78 | ||||
-rw-r--r-- | ts/SettingsStore.ts | 13 | ||||
-rw-r--r-- | ts/Storage.ts | 47 | ||||
-rw-r--r-- | ts/UsersLoader.ts | 13 | ||||
-rw-r--r-- | ts/index.ts | 33 | ||||
-rw-r--r-- | ts/utils/Viewport.ts | 18 |
14 files changed, 587 insertions, 0 deletions
diff --git a/ts/Bootstrap.ts b/ts/Bootstrap.ts new file mode 100644 index 0000000..c8850bf --- /dev/null +++ b/ts/Bootstrap.ts @@ -0,0 +1,33 @@ +import { DEPENDENCIES } from './CONST' + +export default class Bootstrap { + public static start() { + Bootstrap.checkDependencies(); + Bootstrap.checkFrame(); + Bootstrap.checkSpecialPage(); + } + + private static checkDependencies() { + for (let dependency of DEPENDENCIES) { + if (typeof (<any>window)[dependency] === 'undefined') { + throw `Dependency "${dependency}" is missing.`; + } + } + } + + private static checkFrame() { + if (window.parent && window !== window.parent) { + throw `Abort, because we are running inside a frame.`; + } + } + + private static checkSpecialPage() { + if (/^(\/index.php)?\/s\//.test(location.pathname)) { + throw `Abort, because we dont want to start chat on public shares.`; + } + + if (OC.generateUrl('login/flow') === window.location.pathname) { + throw `Abort, because chat is not needed on flow login.`; + } + } +} diff --git a/ts/CONST.ts b/ts/CONST.ts new file mode 100644 index 0000000..9b670cf --- /dev/null +++ b/ts/CONST.ts @@ -0,0 +1,7 @@ +export const SERVER_TYPE = { + INTERNAL: 0, + EXTERNAL: 1, + MANAGED: 2 +}; + +export const DEPENDENCIES = ['jsxc', 'oc_config', 'oc_appswebroots', 'OC']; diff --git a/ts/ChatIconInjector.ts b/ts/ChatIconInjector.ts new file mode 100644 index 0000000..7e7ce9d --- /dev/null +++ b/ts/ChatIconInjector.ts @@ -0,0 +1,11 @@ + +export default function injectChatIcon() { + var div = $('<div/>'); + + div.addClass('jsxc_chatIcon'); + div.click(function() { + jsxc.toggleRoster(); + }); + + $('#header form.searchbox').after(div); +} diff --git a/ts/ChatSubmitButtonInjector.ts b/ts/ChatSubmitButtonInjector.ts new file mode 100644 index 0000000..de04fd6 --- /dev/null +++ b/ts/ChatSubmitButtonInjector.ts @@ -0,0 +1,41 @@ +import Storage from './Storage' + +function addChatSubmitButton(formElement: JQuery<any>) { + let storage = Storage.get(); + let defaultEnable = OJSXC_CONFIG.defaultLoginFormEnable; + let submitWrapperElement = $('<div>'); + submitWrapperElement.attr('id', 'jsxc-submit-wrapper'); + + let submitElement = $('<input>'); + submitElement.attr({ + type: 'button', + id: 'jsxc-submit', + }); + submitElement.addClass('login primary'); + if (defaultEnable) { + submitElement.val('Log_in_without_chat'); //@TODO translate + submitElement.click(function() { + // submit form without login + }); + } else { + submitElement.val('Log_in_with_chat'); + submitElement.click(function() { + let forceLoginFormEnable = true; + formElement.submit(); + }); + } + + submitWrapperElement.append(submitElement); + $('.login-additional').prepend(submitWrapperElement); + + $('#lost-password').mouseup(function(ev) { + ev.preventDefault(); + + submitWrapperElement.slideUp().fadeOut(); + }); + $('#lost-password-back').mouseup(function(ev) { + ev.preventDefault(); + + submitWrapperElement.slideDown().fadeIn(); + }); +} diff --git a/ts/ContactMenuObserver.todo b/ts/ContactMenuObserver.todo new file mode 100644 index 0000000..112af21 --- /dev/null +++ b/ts/ContactMenuObserver.todo @@ -0,0 +1,90 @@ +function observeContactsMenu() { + var target = document.getElementById('contactsmenu'); + + var observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.target.id !== 'contactsmenu-contacts') { + return; + } + + $(mutation.target).find('[href^="xmpp:"]').addClass('jsxc_statusIndicator'); + + $(mutation.target).find('.contact').each(function () { + updateContactItem($(this)); + }); + + jsxc.gui.detectUriScheme(mutation.target); + }); + }); + + var config = { + attributes: true, + childList: true, + characterData: true, + subtree: true + }; + + observer.observe(target, config); +} + +function updateContactItem(contactElement) { + var xmppAddresses = contactElement.find('[href^="xmpp:"]').map(function () { + return $(this).attr('href').replace(/^xmpp:/, ''); + }); + + if (xmppAddresses.length === 0) { + return; + } + + var lastMessages = []; + var highestPresent = jsxc.CONST.STATUS.indexOf('offline'); + var highestPresentBid = xmppAddresses.get(0); + + xmppAddresses.each(function (index, bid) { + var lastMsg = jsxc.getLastMsg(bid); + + if (lastMsg) { + lastMessages.push(lastMsg); + } + + var data = jsxc.storage.getUserItem('buddy', bid) || {}; + + if (data.status > highestPresent) { + highestPresent = data.status; + highestPresentBid = bid; + } + }); + + var latestMsg = { + date: 0 + }; + $(lastMessages).each(function (index, msg) { + if (msg.date > latestMsg.date) { + latestMsg = msg; + } + }); + + if (latestMsg.date > 0) { + // replace emoticons from XEP-0038 and pidgin with shortnames + $.each(jsxc.gui.emotions, function (i, val) { + latestMsg.text = latestMsg.text.replace(val[2], ':' + val[1] + ':'); + }); + + // translate shortnames to images + latestMsg.text = jsxc.gui.shortnameToImage(latestMsg.text); + + contactElement.find('.last-message').html(latestMsg.text); + } + + if (highestPresent > 0) { + var status = jsxc.CONST.STATUS[highestPresent]; + + contactElement.removeClass('jsxc_' + jsxc.CONST.STATUS.join(' jsxc_')).addClass('jsxc_' + status); + } + + if (highestPresentBid) { + contactElement.find('.avatar').click(function () { + jsxc.gui.queryActions.message(highestPresentBid); + }); + } +}
\ No newline at end of file diff --git a/ts/DefaultAvatar.ts b/ts/DefaultAvatar.ts new file mode 100644 index 0000000..1967caf --- /dev/null +++ b/ts/DefaultAvatar.ts @@ -0,0 +1,84 @@ +import Storage from "./Storage"; +import { IJID } from "jsxc/src/JID.interface"; + +enum Presence { + online, + chat, + away, + xa, + dnd, + offline +} + +interface Avatar { + username: string, + type: 'url' | 'placeholder', + displayname?: string, + url?: string; +} + +export default function defaultAvatar(elements, jid: IJID, name: string, presence) { + let storage = Storage.get(); + + let adminSettings = storage.getItem('adminSettings') || {}; + + $(elements).each(function() { + let element = $(this); + + if (element.length === 0) { + return; + } + + let size = <number>element.width(); + let username = jid.node; + let key = username + '@' + size; + let cache = storage.getItem('avatar:' + key); + let isExternalUser = jid.domain !== adminSettings.xmppDomain; + + if (cache) { + displayAvatar(element, cache); + } else if (isExternalUser || presence === Presence.offline) { + setPlaceholder(element, username); + } else { + requestAvatar(element, username, size); + } + }); +} + +function requestAvatar(element: JQuery<any>, username: string, size: number) { + let url = getAvatarUrl(username, size); + + $.get(url, function(result) { + let avatar: Avatar = { + username: username, + type: typeof result === 'string' ? 'url' : 'placeholder', + displayname: result.data && result.data.displayname ? result.data.displayname : undefined, + url: typeof result === 'string' ? result : undefined, + }; + + displayAvatar(element, avatar); + + Storage.get().setItem('avatar:' + username + '@' + size, avatar); + }); +} + +function displayAvatar(element, avatar: Avatar) { + if (avatar.type === 'url') { + element.css('backgroundImage', 'url(' + avatar.url + ')'); + element.text(''); + } else { + setPlaceholder(element, avatar.username, avatar.displayname); + } +} + +function getAvatarUrl(username: string, size: number) { + return OC.generateUrl('/avatar/' + encodeURIComponent(username) + '/' + size + '?requesttoken={requesttoken}', { + user: username, + size: size, + requesttoken: oc_requesttoken + }) +} + +function setPlaceholder(element, username: string, displayname?: string) { + (<any>element).imageplaceholder(username, displayname); +} diff --git a/ts/FormWatcher.ts b/ts/FormWatcher.ts new file mode 100644 index 0000000..bab8b77 --- /dev/null +++ b/ts/FormWatcher.ts @@ -0,0 +1,55 @@ + +export default class FormWatcher { + public static callback(username: string, password: string) { + return new Promise(resolve => { + $.ajax({ + type: 'POST', + url: OC.generateUrl('apps/ojsxc/settings'), + data: { + username: username, + password: password + }, + success: response => FormWatcher.success(response, resolve), + error: xhr => FormWatcher.error(xhr, resolve), + }); + }); + } + + private static success(response, resolve) { + if (response.result !== 'success') { + resolve(false); + + return; + } + + let xmpp = response.data.xmpp || {}; + + if (!xmpp.url) { + resolve(false); + + return; + } + + resolve({ + xmpp: { + url: xmpp.url, + domain: xmpp.domain, + } + }); + } + + private static error(xhr, resolve) { + if (xhr.responseJSON && xhr.responseJSON.message) { + throw 'Error message: ' + xhr.responseJSON.message; + } + + if (xhr.status === 412) { + console.log('Refresh page to get a new CSRF token'); + + window.location.href = window.location.href; + return; + } + + resolve(false); + } +} diff --git a/ts/InternalBackend.todo b/ts/InternalBackend.todo new file mode 100644 index 0000000..903d402 --- /dev/null +++ b/ts/InternalBackend.todo @@ -0,0 +1,64 @@ +$(document).on('click', '#jsxc_roster p', function () { + if (jsxc.storage.getItem('serverType') === serverTypes.INTERNAL) { + startInternalBackend(); + } +}); + +function startInternalBackend() { + var currentUser = OC.currentUser; + + if (!currentUser) { + return; + } + + jsxc.bid = currentUser.toLowerCase() + '@' + window.location.host; + + jsxc.options.set('xmpp', { + url: OC.generateUrl('apps/ojsxc/http-bind') + }); + + $(document).one('attached.jsxc', function () { + if (jsxc.options.get('loginForm').startMinimized !== true) { + jsxc.gui.roster.toggle(jsxc.CONST.SHOWN); + } + }); + + jsxc.start(jsxc.bid + '/internal', 'internal', '123456'); +} + + +if (jsxc.storage.getItem('serverType') === serverTypes.INTERNAL) { + jsxc.gui.showLoginBox = function() {}; + } + + $(document).on('stateChange.jsxc', function _handler(event, state) { + if (state === jsxc.CONST.STATE.SUSPEND) { + /** + * The first time we go into suspend mode we check if we are using the internal backend. + * If this is the case and the user explicitly press the "login_without_chat" button when logging + * into Nextcloud we know we are using another authentication mechanism (like SAML/SSO) and thus have + * to manually start the connection. + */ + var chatDisabledByUser = jsxc.storage.getUserItem('forcedLogout') || jsxc.storage.getItem('login_without_chat'); + $(document).off('stateChange.jsxc', _handler); + if (jsxc.storage.getItem('serverType') === null) { + $.ajax({ + url: OC.generateUrl('apps/ojsxc/settings/servertype'), + success: function(data) { + jsxc.storage.setItem('serverType', serverTypes[data.serverType.toUpperCase()]); + + if (data.serverType === 'internal' && !chatDisabledByUser) { + jsxc.gui.showLoginBox = function() {}; + startInternalBackend(); + } + } + }); + } else if (jsxc.storage.getItem('serverType') === serverTypes.INTERNAL && !chatDisabledByUser) { + jsxc.gui.showLoginBox = function() {}; + startInternalBackend(); + } + } else if (state === jsxc.CONST.STATE.READY) { + // if JSXC is ready this means we successfully connected and thus don't have to listen to the suspend state + $(document).off('stateChange.jsxc', _handler); + } + });
\ No newline at end of file diff --git a/ts/SettingsLoader.ts b/ts/SettingsLoader.ts new file mode 100644 index 0000000..2f4407c --- /dev/null +++ b/ts/SettingsLoader.ts @@ -0,0 +1,78 @@ +import { SERVER_TYPE } from "./CONST"; +import Storage from "./Storage"; + +export function loadSettings(username, password) { + return new Promise((resolve, reject) => { + $.ajax({ + type: 'POST', + url: OC.generateUrl('apps/ojsxc/settings'), + data: { + username: username, + password: password + }, + success: (d) => resolve(d), + error: xhr => reject(xhr), + }); + }) + .then(handleResponse) + .catch(handleError); +} + +function handleResponse(response) { + if (response.result !== 'success' || !response.data) { + throw 'Received unsuccessful response.'; + } + + let data = response.data; + let serverType = SERVER_TYPE[data.serverType.toUpperCase()]; + let xmpp = data.xmpp || {}; + + Storage.get().setItem('serverType', serverType); + + if (serverType !== SERVER_TYPE.INTERNAL && xmpp.url) { + + // if (forceLoginFormEnable) { + // response.data.loginForm.enable = true; + // } + + return { + xmpp: { + url: xmpp.url, + domain: xmpp.domain, + } + }; + } else if (serverType === SERVER_TYPE.INTERNAL) { + // var node = username || OC.currentUser; + // jsxc.bid = node.toLowerCase() + '@' + window.location.host; + + // jsxc.options.set('adminSettings', response.data.adminSettings); + + // if (response.data.loginForm) { + // jsxc.options.set('loginForm', { + // startMinimized: response.data.loginForm.startMinimized + // }); + // } + } + + Storage.get().removeItem('serverType'); + + return false; +} + +function handleError(xhr) { + console.warn('XHR error on getSettings.php'); + + if (xhr.responseJSON && xhr.responseJSON.message) { + console.log('Error message: ' + xhr.responseJSON.message); + } + + if (xhr.status === 412) { + console.log('Refresh page to get a new CSRF token'); + + window.location.href = window.location.href; + + return new Promise(() => { }); + } + + return false; +} diff --git a/ts/SettingsStore.ts b/ts/SettingsStore.ts new file mode 100644 index 0000000..e493f49 --- /dev/null +++ b/ts/SettingsStore.ts @@ -0,0 +1,13 @@ +function saveSettinsPermanent(data, cb) { + $.ajax({ + type: 'POST', + url: OC.generateUrl('apps/ojsxc/settings/user'), + data: data, + success: function(data) { + cb(data && data.status === 'success'); + }, + error: function() { + cb(false); + } + }); +} diff --git a/ts/Storage.ts b/ts/Storage.ts new file mode 100644 index 0000000..9d3d8a9 --- /dev/null +++ b/ts/Storage.ts @@ -0,0 +1,47 @@ + +const PREFIX = 'ojsxc:'; + +const BACKEND = localStorage; + +export default class Storage { + + private static instance; + + public static get(): Storage { + if (!Storage.instance) { + Storage.instance = new Storage(); + } + + return Storage.instance; + } + + private constructor() { + + } + + public setItem(key: string, value: any): void { + try { + value = JSON.stringify(value); + } catch (err) { + console.warn('Error while stringifing', err); + + return; + } + + BACKEND.setItem(PREFIX + key, value); + } + + public getItem(key: string): any { + let value = BACKEND.getItem(PREFIX + key); + + if (typeof value === 'string') { + value = JSON.parse(value); + } + + return value; + } + + public removeItem(key: string): void { + BACKEND.removeItem(PREFIX + key); + } +} diff --git a/ts/UsersLoader.ts b/ts/UsersLoader.ts new file mode 100644 index 0000000..fb48f83 --- /dev/null +++ b/ts/UsersLoader.ts @@ -0,0 +1,13 @@ +function getUsers(search, cb) { + $.ajax({ + type: 'GET', + url: OC.generateUrl('apps/ojsxc/settings/users'), + data: { + search: search + }, + success: cb, + error: function() { + console.warn('XHR error on getUsers.php'); + } + }); +} diff --git a/ts/index.ts b/ts/index.ts new file mode 100644 index 0000000..4c0bf0a --- /dev/null +++ b/ts/index.ts @@ -0,0 +1,33 @@ +// import jsxc from 'jsxc/src'; +import FormWatcher from './FormWatcher'; +import Bootstrap from './Bootstrap'; +import { loadSettings } from './SettingsLoader'; +import injectChatIcon from './ChatIconInjector'; + +(function() { + try { + Bootstrap.start(); + } catch (err) { + console.warn('Abort JSXC', err); + + return; + } + + let numberOfCachedAccounts = jsxc.init({ + loadSettings: loadSettings, + }); + + if (numberOfCachedAccounts === 0 && oc_current_user) { + jsxc.start(); + } + + injectChatIcon(); + + let formElement = $('#body-login form'); + let usernameElement = $('#user'); + let passwordElement = $('#password'); + + if (formElement.length > 0) { + jsxc.watchForm(formElement, usernameElement, passwordElement, FormWatcher.callback); + } +})(); diff --git a/ts/utils/Viewport.ts b/ts/utils/Viewport.ts new file mode 100644 index 0000000..0c2f37a --- /dev/null +++ b/ts/utils/Viewport.ts @@ -0,0 +1,18 @@ +import Storage from '../Storage' + +export default class Viewport { + public static getSize(): { width: number, height: number } { + var w = <number>$(window).width() - <number>$('#jsxc_windowListSB').width(); + var h = <number>$(window).height() - <number>$('#header').height() - 10; + + //@TODO + if (Storage.get().getItem('roster') === 'shown') { + w -= <number>$('#jsxc-roster').outerWidth(true); + } + + return { + width: w, + height: h + }; + } +} |