Add files
This commit is contained in:
commit
bb80829159
18195 changed files with 2122994 additions and 0 deletions
43
509bba0_unpacked/discord_app/utils/ActivityUtils.js
Executable file
43
509bba0_unpacked/discord_app/utils/ActivityUtils.js
Executable file
|
@ -0,0 +1,43 @@
|
|||
/* @flow */
|
||||
|
||||
import {ActivityTypes} from '../Constants';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export type Activity = {
|
||||
type?: number,
|
||||
name: string,
|
||||
url?: ?string,
|
||||
};
|
||||
|
||||
export function renderActivity(activity: ?Activity): ?ReactElement {
|
||||
if (activity == null || activity.name == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isStreaming(activity)) {
|
||||
return i18n.Messages.STREAMING.format({name: activity.name});
|
||||
}
|
||||
|
||||
return i18n.Messages.PLAYING_GAME.format({game: activity.name});
|
||||
}
|
||||
|
||||
const validStreamUrl = /^https?:\/\/(www\.)?twitch\.tv\/.+/;
|
||||
|
||||
export function isStreaming(activity: ?Activity): boolean {
|
||||
if (activity == null) return false;
|
||||
if (activity.type !== ActivityTypes.STREAMING) return false;
|
||||
if (activity.url == null) return false;
|
||||
return validStreamUrl.test(activity.url);
|
||||
}
|
||||
|
||||
export function getStreamURL(activity: ?Activity): ?string {
|
||||
if (activity != null && activity.url != null && validStreamUrl.test(activity.url)) {
|
||||
return activity.url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ActivityUtils.js
|
315
509bba0_unpacked/discord_app/utils/AnalyticsUtils.js
Executable file
315
509bba0_unpacked/discord_app/utils/AnalyticsUtils.js
Executable file
|
@ -0,0 +1,315 @@
|
|||
import {AnalyticEvents, Endpoints} from '../Constants';
|
||||
import HTTPUtils from './HTTPUtils';
|
||||
import {Buffer} from 'buffer';
|
||||
import Storage from '../lib/Storage';
|
||||
|
||||
const MINUTES_15 = 15 * 60 * 1000;
|
||||
|
||||
let token;
|
||||
let superProperties;
|
||||
const throttledEvents = {};
|
||||
|
||||
// Repeat some NativeUtils stuff rather than importing NativeUtils because this
|
||||
// file is shared with the marketing page.
|
||||
const __require = window.__require;
|
||||
if (__require != null) {
|
||||
let electron;
|
||||
try {
|
||||
electron = __require('electron');
|
||||
} catch (e) {}
|
||||
|
||||
if (electron) {
|
||||
const os = __require('os');
|
||||
const process = __require('process');
|
||||
|
||||
let osName;
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
osName = 'Windows';
|
||||
break;
|
||||
case 'darwin':
|
||||
osName = 'Mac OS X';
|
||||
break;
|
||||
case 'linux':
|
||||
osName = 'Linux';
|
||||
break;
|
||||
default:
|
||||
osName = process.platform;
|
||||
break;
|
||||
}
|
||||
|
||||
superProperties = {
|
||||
os: osName,
|
||||
browser: 'Discord Client',
|
||||
/* eslint-disable camelcase */
|
||||
release_channel: electron.remote.getGlobal('releaseChannel') || 'unknown',
|
||||
client_version: electron.remote.app.getVersion(),
|
||||
os_version: os.release(),
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
|
||||
if (process.platform == 'linux') {
|
||||
const metadata = electron.remote.getGlobal('crashReporterMetadata');
|
||||
superProperties['window_manager'] = metadata['wm'];
|
||||
superProperties['distro'] = metadata['distro'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CAMPAIGN_KEYWORDS = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' ');
|
||||
|
||||
function getQueryParam(url, param) {
|
||||
param = param.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
|
||||
const regex = new RegExp(`[\\?&]${param}=([^&#]*)`);
|
||||
const results = regex.exec(url);
|
||||
if (results === null || (results && typeof results[1] !== 'string' && results[1].length)) {
|
||||
return '';
|
||||
} else {
|
||||
return decodeURIComponent(results[1]).replace(/\+/g, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
function getCampaignParams(properties = {}) {
|
||||
CAMPAIGN_KEYWORDS.forEach(key => {
|
||||
const value = getQueryParam(document.URL, key);
|
||||
if (value.length) {
|
||||
properties[key] = value;
|
||||
}
|
||||
});
|
||||
return properties;
|
||||
}
|
||||
|
||||
function getSearchEngine() {
|
||||
const referrer = document.referrer;
|
||||
if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
|
||||
return 'google';
|
||||
} else if (referrer.search('https?://(.*)bing.com') === 0) {
|
||||
return 'bing';
|
||||
} else if (referrer.search('https?://(.*)yahoo.com') === 0) {
|
||||
return 'yahoo';
|
||||
} else if (referrer.search('https?://(.*)duckduckgo.com') === 0) {
|
||||
return 'duckduckgo';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSearchInfo(properties = {}) {
|
||||
const referrer = document.referrer;
|
||||
const search = getSearchEngine(referrer);
|
||||
const param = search !== 'yahoo' ? 'q' : 'p';
|
||||
if (search != null) {
|
||||
properties['search_engine'] = search;
|
||||
const keyword = getQueryParam(referrer, param);
|
||||
if (keyword.length) {
|
||||
properties['mp_keyword'] = keyword;
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
function getBrowser() {
|
||||
const userAgent = navigator.userAgent;
|
||||
const vendor = navigator.vendor || '';
|
||||
const opera = window.opera;
|
||||
if (__SDK__) {
|
||||
return 'Discord SDK';
|
||||
} else if (__IOS__) {
|
||||
return 'Discord iOS';
|
||||
} else if (opera) {
|
||||
return /Mini/.test(userAgent) ? 'Opera Mini' : 'Opera';
|
||||
} else if (/(BlackBerry|PlayBook|BB10)/i.test(userAgent)) {
|
||||
return 'BlackBerry';
|
||||
} else if (/FBIOS/.test(userAgent)) {
|
||||
return 'Facebook Mobile';
|
||||
} else if (/CriOS/.test(userAgent)) {
|
||||
return 'Chrome iOS';
|
||||
} else if (/Apple/.test(vendor)) {
|
||||
return /Mobile/.test(userAgent) ? 'Mobile Safari' : 'Safari';
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
return /Chrome/.test(userAgent) ? 'Android Chrome' : 'Android Mobile';
|
||||
} else if (/Chrome/.test(userAgent)) {
|
||||
return 'Chrome';
|
||||
} else if (/Konqueror/.test(userAgent)) {
|
||||
return 'Konqueror';
|
||||
} else if (/Firefox/.test(userAgent)) {
|
||||
return 'Firefox';
|
||||
} else if (/MSIE|Trident\//.test(userAgent)) {
|
||||
return 'Internet Explorer';
|
||||
} else if (/Gecko/.test(userAgent)) {
|
||||
return 'Mozilla';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (/Windows/i.test(userAgent)) {
|
||||
return /Phone/.test(userAgent) ? 'Windows Mobile' : 'Windows';
|
||||
} else if (/(iPhone|iPad|iPod)/.test(userAgent) || __IOS__) {
|
||||
return 'iOS';
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
return 'Android';
|
||||
} else if (/(BlackBerry|PlayBook|BB10)/i.test(userAgent)) {
|
||||
return 'BlackBerry';
|
||||
} else if (/Mac/i.test(userAgent)) {
|
||||
return 'Mac OS X';
|
||||
} else if (/Linux/i.test(userAgent)) {
|
||||
return 'Linux';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getDevice() {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (/iPad/.test(userAgent)) {
|
||||
return 'iPad';
|
||||
} else if (/iPod/.test(userAgent)) {
|
||||
return 'iPod Touch';
|
||||
} else if (/iPhone/.test(userAgent) || __IOS__) {
|
||||
return 'iPhone';
|
||||
} else if (/(BlackBerry|PlayBook|BB10)/i.test(userAgent)) {
|
||||
return 'BlackBerry';
|
||||
} else if (/Windows Phone/i.test(userAgent)) {
|
||||
return 'Windows Phone';
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
return 'Android';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getReferringDomain() {
|
||||
const split = document.referrer.split('/');
|
||||
return split.length >= 3 ? split[2] : '';
|
||||
}
|
||||
|
||||
function getSuperProperties(properties = {}) {
|
||||
properties['os'] = getOS();
|
||||
properties['browser'] = getBrowser();
|
||||
properties['device'] = getDevice();
|
||||
if (__WEB__) {
|
||||
properties['referrer'] = document.referrer;
|
||||
properties['referring_domain'] = getReferringDomain();
|
||||
getCampaignParams(properties);
|
||||
getSearchInfo(properties);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
function getCachedSuperProperties() {
|
||||
let properties = Storage.get('superProperties');
|
||||
if (!properties) {
|
||||
properties = getSuperProperties();
|
||||
Storage.set('superProperties', properties);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
if (!superProperties) {
|
||||
try {
|
||||
superProperties = getCachedSuperProperties();
|
||||
} catch (e) {
|
||||
superProperties = {};
|
||||
}
|
||||
}
|
||||
|
||||
function encodeProperties(properties: {[key: string]: any}): ?string {
|
||||
try {
|
||||
return new Buffer(JSON.stringify(properties)).toString('base64');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const superPropertiesBase64 = encodeProperties(superProperties);
|
||||
|
||||
type AnalyticEventConfig = {
|
||||
throttlePeriod: number,
|
||||
throttleKeys: (properties: Object) => string[],
|
||||
};
|
||||
|
||||
// @flow
|
||||
export const ANALYTIC_EVENT_CONFIGS: {[key: string]: AnalyticEventConfig} = {
|
||||
[AnalyticEvents.ACK_MESSAGES]: {
|
||||
throttlePeriod: MINUTES_15,
|
||||
throttleKeys: prop => [prop.guild_id],
|
||||
},
|
||||
[AnalyticEvents.START_SPEAKING]: {
|
||||
throttlePeriod: MINUTES_15,
|
||||
throttleKeys: prop => [prop.server],
|
||||
},
|
||||
[AnalyticEvents.START_LISTENING]: {
|
||||
throttlePeriod: MINUTES_15,
|
||||
throttleKeys: prop => [prop.server],
|
||||
},
|
||||
test: 2,
|
||||
};
|
||||
|
||||
function isThrottled(namedKey) {
|
||||
return throttledEvents[namedKey] && throttledEvents[namedKey] > Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send event to API server to track on multiple services.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Object} [properties]
|
||||
*/
|
||||
function track(event: string, properties: Object = {}) {
|
||||
const config: ?AnalyticEventConfig = ANALYTIC_EVENT_CONFIGS[event];
|
||||
|
||||
if (config) {
|
||||
const throttleKey = [event, ...config.throttleKeys(properties)].join('_');
|
||||
if (isThrottled(throttleKey)) {
|
||||
return;
|
||||
}
|
||||
throttledEvents[throttleKey] = Date.now() + config.throttlePeriod;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info('AnalyticsUtils.track(...):', event, properties);
|
||||
}
|
||||
|
||||
return HTTPUtils.post({
|
||||
url: Endpoints.TRACK,
|
||||
body: {
|
||||
event,
|
||||
properties,
|
||||
token,
|
||||
},
|
||||
retries: 3,
|
||||
}).catch(e => {
|
||||
console.error('AnalyticsUtils.track(...):', e.body.message);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
track,
|
||||
isThrottled,
|
||||
|
||||
encodeProperties,
|
||||
|
||||
setToken(newToken) {
|
||||
token = newToken;
|
||||
},
|
||||
|
||||
clearToken() {
|
||||
token = null;
|
||||
},
|
||||
|
||||
getSuperProperties() {
|
||||
return superProperties;
|
||||
},
|
||||
|
||||
getSuperPropertiesBase64() {
|
||||
return superPropertiesBase64;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/AnalyticsUtils.js
|
51
509bba0_unpacked/discord_app/utils/AnimationManager.js
Executable file
51
509bba0_unpacked/discord_app/utils/AnimationManager.js
Executable file
|
@ -0,0 +1,51 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import ScriptLoaderUtils from './ScriptLoaderUtils';
|
||||
|
||||
const ANIMATIONS = new Map();
|
||||
|
||||
function registerAnimation(name, data) {
|
||||
if (name && data) {
|
||||
ANIMATIONS.set(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
window.discordRegisterAnimation = registerAnimation;
|
||||
|
||||
function loadAnimation(name, url) {
|
||||
if (hasAnimation(name)) {
|
||||
return new Promise(resolve => resolve(getAnimation(name)));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ScriptLoaderUtils.ensure(url).then(() => {
|
||||
if (hasAnimation(name)) {
|
||||
return resolve(getAnimation(name));
|
||||
}
|
||||
reject();
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function hasAnimation(name) {
|
||||
return ANIMATIONS.has(name);
|
||||
}
|
||||
|
||||
function getAnimation(name) {
|
||||
return ANIMATIONS.get(name);
|
||||
}
|
||||
|
||||
function unloadAnimation(name) {
|
||||
ANIMATIONS.delete(name);
|
||||
}
|
||||
|
||||
export default {
|
||||
get: getAnimation,
|
||||
load: loadAnimation,
|
||||
has: hasAnimation,
|
||||
register: registerAnimation,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/AnimationManager.js
|
129
509bba0_unpacked/discord_app/utils/AppAnalyticsUtils.js
Executable file
129
509bba0_unpacked/discord_app/utils/AppAnalyticsUtils.js
Executable file
|
@ -0,0 +1,129 @@
|
|||
/* @flow */
|
||||
|
||||
import {ChannelTypes, GuildFeatures, Permissions} from '../Constants';
|
||||
import AuthenticationStore from '../stores/AuthenticationStore';
|
||||
import SelectedChannelStore from '../stores/SelectedChannelStore';
|
||||
import SelectedGuildStore from '../stores/SelectedGuildStore';
|
||||
import ChannelStore from '../stores/ChannelStore';
|
||||
import ChannelMemberStore from '../stores/views/ChannelMemberStore';
|
||||
import GuildChannelStore from '../stores/views/GuildChannelStore';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import GuildMemberStore from '../stores/GuildMemberStore';
|
||||
import GuildMemberCountStore from '../stores/GuildMemberCountStore';
|
||||
import PermissionStore from '../stores/PermissionStore';
|
||||
import AnalyticsUtils from './AnalyticsUtils';
|
||||
|
||||
type GuildAnalyticsMetaData = {
|
||||
guild_id: string,
|
||||
guild_size_total: ?number,
|
||||
|
||||
// guild_size_online: number;
|
||||
guild_member_num_roles: number,
|
||||
guild_member_perms: number,
|
||||
|
||||
guild_num_channels: number,
|
||||
guild_num_text_channels: number,
|
||||
guild_num_voice_channels: number,
|
||||
guild_num_roles: number,
|
||||
|
||||
guild_is_vip: boolean,
|
||||
};
|
||||
|
||||
type ChannelAnalyticsMetaData = {
|
||||
channel_id: string,
|
||||
channel_type: number,
|
||||
|
||||
channel_size_total: number, // for group DMs
|
||||
channel_size_online: ?number,
|
||||
|
||||
channel_member_perms: number,
|
||||
channel_hidden: boolean,
|
||||
};
|
||||
|
||||
function countKeys(object: Object) {
|
||||
let keys = 0;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const _ in object) {
|
||||
keys += 1;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function collectGuildAnalyticsMetadata(guildId: ?string): ?GuildAnalyticsMetaData {
|
||||
if (guildId == null) return null;
|
||||
|
||||
const guild = GuildStore.getGuild(guildId);
|
||||
if (guild == null) return null;
|
||||
|
||||
const userId = AuthenticationStore.getId();
|
||||
const guildMember = GuildMemberStore.getMember(guildId, userId);
|
||||
const guildChannels = GuildChannelStore.getChannels(guildId);
|
||||
|
||||
return {
|
||||
/* eslint-disable camelcase */
|
||||
guild_id: guild.id,
|
||||
guild_size_total: GuildMemberCountStore.getMemberCount(guildId),
|
||||
guild_num_channels: guildChannels.count,
|
||||
guild_num_text_channels: guildChannels[ChannelTypes.GUILD_TEXT].length,
|
||||
guild_num_voice_channels: guildChannels[ChannelTypes.GUILD_VOICE].length,
|
||||
// (optional): low pri
|
||||
guild_num_roles: countKeys(guild.roles),
|
||||
guild_member_num_roles: guildMember ? guildMember.roles.length : 0,
|
||||
guild_member_perms: PermissionStore.getGuildPermissions(guildId) || 0,
|
||||
guild_is_vip: guild.hasFeature(GuildFeatures.VIP_REGIONS),
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
}
|
||||
|
||||
function collectChannelAnalyticsMetadata(channelId: ?string): ?ChannelAnalyticsMetaData {
|
||||
if (channelId == null) return null;
|
||||
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel == null) return null;
|
||||
let channelHidden = false;
|
||||
|
||||
const guildId = channel.getGuildId();
|
||||
if (guildId != null) {
|
||||
const everyoneOverwrite = channel.permissionOverwrites[guildId];
|
||||
if (everyoneOverwrite && (everyoneOverwrite.deny & Permissions.READ_MESSAGES) === 0) {
|
||||
channelHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/* eslint-disable camelcase */
|
||||
channel_id: channel.id,
|
||||
channel_type: channel.type,
|
||||
channel_size_total: guildId ? 0 : channel.recipients.length,
|
||||
channel_size_online: ChannelMemberStore.getOnlineMemberCount(channelId),
|
||||
channel_member_perms: guildId ? PermissionStore.getChannelPermissions(channelId) || 0 : 0,
|
||||
channel_hidden: channelHidden,
|
||||
/* eslint-enable camelcase */
|
||||
};
|
||||
}
|
||||
|
||||
function trackWithMetadata(event: string, properties: Object = {}, throttle: number = 0) {
|
||||
if (AnalyticsUtils.isThrottled(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = properties.guild_id || SelectedGuildStore.getGuildId();
|
||||
const channelId = properties.channel_id || SelectedChannelStore.getChannelId(guildId);
|
||||
|
||||
const propertiesWithMetadata = {
|
||||
...properties,
|
||||
...collectGuildAnalyticsMetadata(guildId),
|
||||
...collectChannelAnalyticsMetadata(channelId),
|
||||
};
|
||||
|
||||
AnalyticsUtils.track(event, propertiesWithMetadata, throttle);
|
||||
}
|
||||
|
||||
export default {
|
||||
trackWithMetadata,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/AppAnalyticsUtils.js
|
68
509bba0_unpacked/discord_app/utils/AppUtils.js
Executable file
68
509bba0_unpacked/discord_app/utils/AppUtils.js
Executable file
|
@ -0,0 +1,68 @@
|
|||
import Dispatcher from '../Dispatcher';
|
||||
import SelectedChannelActionCreators from '../actions/SelectedChannelActionCreators';
|
||||
import GuildActionCreators from '../actions/GuildActionCreators';
|
||||
import AnalyticsUtils from './AnalyticsUtils';
|
||||
import RouterUtils from './RouterUtils';
|
||||
import {AnalyticEvents, ME, Routes} from '../Constants';
|
||||
|
||||
function isValidGuildId(guildId) {
|
||||
return guildId === ME || /^\d+$/.test(guildId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a Flux anti-pattern. When using the HTML5 History mode on the router
|
||||
* and forcing a location then it is done synchronously which might mount
|
||||
* new components which might want to trigger load actions.2
|
||||
*/
|
||||
function dispatch(dispatchFunct) {
|
||||
if (Dispatcher.isDispatching()) {
|
||||
process.nextTick(dispatchFunct);
|
||||
} else {
|
||||
dispatchFunct();
|
||||
}
|
||||
}
|
||||
|
||||
export function routerOnUpdate() {
|
||||
const {routes, params} = this.state;
|
||||
const {query} = this.state.location;
|
||||
|
||||
if (query.uuid) {
|
||||
/* eslint-disable camelcase */
|
||||
const {channelId: channel_id, guildId: guild_id} = params;
|
||||
|
||||
const {uuid: tracking_id, utm_campaign, utm_content, utm_medium, utm_source, utm_term, ...unusedQuery} = query;
|
||||
|
||||
const properties = {
|
||||
tracking_id,
|
||||
channel_id,
|
||||
guild_id: isValidGuildId(guild_id) && guild_id !== ME ? guild_id : null,
|
||||
utm_campaign,
|
||||
utm_content,
|
||||
utm_medium,
|
||||
utm_source,
|
||||
utm_term,
|
||||
path: (routes[1] || {}).path, // just use the highest level route besides root
|
||||
};
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
AnalyticsUtils.track(AnalyticEvents.EMAIL_CLICKED, properties);
|
||||
|
||||
const newLocation = Object.assign({}, this.state.location, {query: unusedQuery});
|
||||
dispatch(() => RouterUtils.replaceWith(newLocation));
|
||||
} else if (routes.some(r => r.componentType === 'channels')) {
|
||||
dispatch(() => {
|
||||
const newGuildId = params.guildId || ME;
|
||||
if (isValidGuildId(newGuildId)) {
|
||||
GuildActionCreators.selectGuild(newGuildId);
|
||||
SelectedChannelActionCreators.selectChannel(newGuildId, params.channelId, query.jump);
|
||||
} else {
|
||||
RouterUtils.replaceWith(Routes.ME);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/AppUtils.js
|
289
509bba0_unpacked/discord_app/utils/AutocompleteUtils.js
Executable file
289
509bba0_unpacked/discord_app/utils/AutocompleteUtils.js
Executable file
|
@ -0,0 +1,289 @@
|
|||
import lodash from 'lodash';
|
||||
import GuildMemberStore from '../stores/GuildMemberStore';
|
||||
import ChannelStore from '../stores/ChannelStore';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import MessageStore from '../stores/MessageStore';
|
||||
import SelectedChannelStore from '../stores/SelectedChannelStore';
|
||||
import SelectedGuildStore from '../stores/SelectedGuildStore';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import GuildChannelStore from '../stores/views/GuildChannelStore';
|
||||
import PrivateChannelSortStore from '../stores/views/PrivateChannelSortStore';
|
||||
import RelationshipStore from '../stores/RelationshipStore';
|
||||
import PermissionUtils from './PermissionUtils';
|
||||
import GuildUtils from './GuildUtils';
|
||||
import RegexUtils from './RegexUtils';
|
||||
import fuzzysearch from 'fuzzysearch';
|
||||
import {sortByMatchScore} from '../lib/SortingUtils';
|
||||
import {Permissions, ChannelTypes} from '../Constants';
|
||||
|
||||
const EXACT_MATCH_VALUE = 10;
|
||||
const CONTAIN_MATCH_VALUE = 5;
|
||||
const FUZZY_MATCH_VALUE = 1;
|
||||
|
||||
const FUZZY_MAX = 50;
|
||||
const NOOP = () => true;
|
||||
|
||||
function getMatchValue(name, exactQuery, containQuery, fuzzyQuery, fuzzy = true) {
|
||||
if (exactQuery.test(name)) {
|
||||
return EXACT_MATCH_VALUE;
|
||||
}
|
||||
if (containQuery.test(name)) {
|
||||
return CONTAIN_MATCH_VALUE;
|
||||
}
|
||||
if (fuzzy && fuzzysearch(fuzzyQuery, name)) {
|
||||
return FUZZY_MATCH_VALUE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function queryMemberList(query, members, limit, filter) {
|
||||
const users = UserStore.getUsers();
|
||||
const selectedGuild = SelectedGuildStore.getGuildId();
|
||||
query = query.toLocaleLowerCase();
|
||||
const queryLen = query.length;
|
||||
let regexResults = [];
|
||||
const fuzzyResults = [];
|
||||
|
||||
const membersLen = members.length;
|
||||
let x = 0;
|
||||
let fuzzyLen = 0;
|
||||
while (x < membersLen) {
|
||||
const result = members[x];
|
||||
let nick;
|
||||
let user;
|
||||
if (result.userId) {
|
||||
nick = result.nick;
|
||||
user = users[result.userId];
|
||||
} else {
|
||||
user = result;
|
||||
nick = GuildMemberStore.getNick(selectedGuild, user.id);
|
||||
}
|
||||
if (!filter || filter(user)) {
|
||||
const usernameLower = user.usernameLowerCase;
|
||||
const nickLower = nick != null ? nick.toLocaleLowerCase() : null;
|
||||
if (usernameLower.substr(0, queryLen) === query || (nick && nickLower.substr(0, queryLen) === query)) {
|
||||
regexResults.push({
|
||||
comparator: nickLower || usernameLower,
|
||||
score: EXACT_MATCH_VALUE,
|
||||
nick,
|
||||
user,
|
||||
});
|
||||
} else if (
|
||||
fuzzyLen < FUZZY_MAX &&
|
||||
(fuzzysearch(query, usernameLower) || (nick && fuzzysearch(query, nickLower)))
|
||||
) {
|
||||
fuzzyResults.push({
|
||||
comparator: nickLower || usernameLower,
|
||||
score: FUZZY_MATCH_VALUE,
|
||||
nick,
|
||||
user,
|
||||
});
|
||||
fuzzyLen += 1;
|
||||
}
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
|
||||
regexResults.sort(sortByMatchScore);
|
||||
if (regexResults.length < limit) {
|
||||
fuzzyResults.sort(sortByMatchScore);
|
||||
regexResults = regexResults.concat(fuzzyResults.slice(0, Math.max(0, limit - regexResults.length)));
|
||||
}
|
||||
|
||||
if (regexResults.length > limit) {
|
||||
regexResults.length = limit;
|
||||
}
|
||||
return regexResults;
|
||||
}
|
||||
|
||||
function getRecentlyTalked(channelId, limit) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
return lodash(MessageStore.getMessages(channelId).toArray())
|
||||
.reverse()
|
||||
.map(message => message.author)
|
||||
.reject(user => user.isNonUserBot())
|
||||
.uniqBy(user => user.id)
|
||||
.map(user => {
|
||||
const member = GuildMemberStore.getMember(channel.getGuildId(), user.id) || {};
|
||||
return {
|
||||
// Nick is now a deprecated API
|
||||
nick: member.nick || null,
|
||||
score: 0,
|
||||
comparator: member.nick || user.username,
|
||||
user,
|
||||
};
|
||||
})
|
||||
.take(limit)
|
||||
.value();
|
||||
}
|
||||
|
||||
export default {
|
||||
queryFriends(query, limit = 10, fuzzy = true, filter) {
|
||||
return queryMemberList(query, RelationshipStore.getFriendIDs().map(id => UserStore.getUser(id)), limit, filter);
|
||||
},
|
||||
|
||||
queryDMUsers(query, limit = 10, fuzzy = true, filter) {
|
||||
return queryMemberList(query, ChannelStore.getDMUserIds().map(id => UserStore.getUser(id)), limit, filter);
|
||||
},
|
||||
|
||||
queryChannelUsers(channelId, query, limit = 10, request = true) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel == null) {
|
||||
return [];
|
||||
}
|
||||
let members;
|
||||
if (channel.isPrivate()) {
|
||||
members = channel.recipients.map(userId => ({userId, nick: channel.nicks[userId]}));
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
members.push({
|
||||
userId: currentUser.id,
|
||||
nick: channel.nicks[currentUser.id],
|
||||
});
|
||||
} else if (query.length === 0) {
|
||||
// If there is no query then just suggest people who have recently talked in the channel.
|
||||
return getRecentlyTalked(channel.id, limit);
|
||||
} else {
|
||||
members = GuildMemberStore.getMembers(channel['guild_id']);
|
||||
if (request) {
|
||||
GuildUtils.requestMembers(channel['guild_id'], query, limit);
|
||||
}
|
||||
}
|
||||
return queryMemberList(
|
||||
query,
|
||||
members,
|
||||
limit,
|
||||
user => channel.isPrivate() || PermissionUtils.can(Permissions.READ_MESSAGES, user, channel)
|
||||
);
|
||||
},
|
||||
|
||||
queryGuildUsers(guildId, query, limit = 10, request = true, filter) {
|
||||
if (GuildStore.getGuild(guildId) == null) {
|
||||
return [];
|
||||
}
|
||||
if (query.length === 0) {
|
||||
return getRecentlyTalked(SelectedChannelStore.getChannelId(guildId), limit);
|
||||
}
|
||||
const members = GuildMemberStore.getMembers(guildId);
|
||||
if (request && query.length > 0) {
|
||||
GuildUtils.requestMembers(guildId, query, limit);
|
||||
}
|
||||
return queryMemberList(query, members, limit, filter);
|
||||
},
|
||||
|
||||
queryUsers(query, limit = 10, fuzzy = true, request = true, filter) {
|
||||
if (request && query.length > 0) {
|
||||
GuildUtils.requestMembers(null, query, limit);
|
||||
}
|
||||
return queryMemberList(query, lodash(UserStore.getUsers()).values().value(), limit, filter);
|
||||
},
|
||||
|
||||
queryChannels(query, guildId, limit = 10, fuzzy = true, filter = NOOP, type = ChannelTypes.GUILD_TEXT) {
|
||||
query = query.toLocaleLowerCase();
|
||||
const exactQuery = new RegExp(`^${RegexUtils.escape(query)}`, 'i');
|
||||
const containQuery = new RegExp(RegexUtils.escape(query), 'i');
|
||||
const user = UserStore.getCurrentUser();
|
||||
let channels;
|
||||
if (guildId) {
|
||||
channels = lodash(GuildChannelStore.getChannels(guildId)[type]).map(obj => obj.channel);
|
||||
} else {
|
||||
channels = lodash(ChannelStore.getChannels()).values().filter(channel => channel.type === type);
|
||||
}
|
||||
|
||||
// Always filter out autocomplete possibilities if user isn't
|
||||
// permitted to see or connect to the text channel
|
||||
if (type === ChannelTypes.GUILD_TEXT) {
|
||||
channels = channels.filter(channel => PermissionUtils.can(Permissions.READ_MESSAGES, user, channel));
|
||||
} else if (type === ChannelTypes.GUILD_VOICE) {
|
||||
channels = channels.filter(channel => PermissionUtils.can(Permissions.CONNECT, user, channel));
|
||||
}
|
||||
|
||||
return channels
|
||||
.filter(filter)
|
||||
.map(channel => {
|
||||
const guild = GuildStore.getGuild(channel.guild_id);
|
||||
let name;
|
||||
if (!guildId) {
|
||||
name = `${channel.toString()} ${guild.toString()}`;
|
||||
} else {
|
||||
name = channel.toString();
|
||||
}
|
||||
name = name.toLocaleLowerCase();
|
||||
const score = getMatchValue(name, exactQuery, containQuery, query, fuzzy);
|
||||
if (score > 0) {
|
||||
return {
|
||||
channel,
|
||||
score,
|
||||
comparator: channel.toString(),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(found => (found ? true : false))
|
||||
.sortBy(({channel}) => channel.name)
|
||||
.sort(sortByMatchScore)
|
||||
.take(limit)
|
||||
.value();
|
||||
},
|
||||
|
||||
queryGuilds(query, limit = 10, fuzzy = true, filter = NOOP) {
|
||||
query = query.toLocaleLowerCase();
|
||||
const exactQuery = new RegExp(`^${RegexUtils.escape(query)}`, 'i');
|
||||
const containQuery = new RegExp(RegexUtils.escape(query), 'i');
|
||||
return lodash(GuildStore.getGuilds())
|
||||
.values()
|
||||
.filter(filter)
|
||||
.map(guild => {
|
||||
const score = getMatchValue(guild.name.toLocaleLowerCase(), exactQuery, containQuery, query, fuzzy);
|
||||
if (score > 0) {
|
||||
return {
|
||||
guild,
|
||||
score,
|
||||
comparator: guild.toString(),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(found => (found ? true : false))
|
||||
.sortBy(guild => guild.name)
|
||||
.sort(sortByMatchScore)
|
||||
.take(limit)
|
||||
.value();
|
||||
},
|
||||
|
||||
queryGroupDMs(query, limit = 10, fuzzy = true, filter = NOOP) {
|
||||
query = query.toLocaleLowerCase();
|
||||
const exactQuery = new RegExp(`^${RegexUtils.escape(query)}`, 'i');
|
||||
const containQuery = new RegExp(RegexUtils.escape(query), 'i');
|
||||
const privateChannelIds = PrivateChannelSortStore.getPrivateChannelIds();
|
||||
const channels = ChannelStore.getChannels();
|
||||
|
||||
return lodash(privateChannelIds)
|
||||
.map(channelId => channels[channelId])
|
||||
.map(channel => {
|
||||
if (channel.type !== ChannelTypes.GROUP_DM) {
|
||||
return null;
|
||||
}
|
||||
const name = channel.toString().toLocaleLowerCase();
|
||||
const score = getMatchValue(name, exactQuery, containQuery, query, fuzzy);
|
||||
if (score > 0) {
|
||||
return {
|
||||
channel,
|
||||
score,
|
||||
comparator: channel.toString(),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(found => (found ? true : false))
|
||||
.filter(filter)
|
||||
.sort(sortByMatchScore)
|
||||
.value();
|
||||
},
|
||||
|
||||
getRecentlyTalked,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/AutocompleteUtils.js
|
196
509bba0_unpacked/discord_app/utils/AvatarUtils.js
Executable file
196
509bba0_unpacked/discord_app/utils/AvatarUtils.js
Executable file
|
@ -0,0 +1,196 @@
|
|||
/* @flow */
|
||||
|
||||
import {Endpoints, Colors, AVATAR_SIZE} from '../Constants';
|
||||
import NativeUtils from '../utils/NativeUtils';
|
||||
|
||||
export type AvatarOptions = {
|
||||
id: string,
|
||||
avatar: ?string,
|
||||
discriminator: string,
|
||||
bot?: boolean,
|
||||
};
|
||||
|
||||
export type AppIconOptions = {
|
||||
applicationId: string,
|
||||
icon: ?string,
|
||||
};
|
||||
|
||||
export type IconOptions = {
|
||||
id: string,
|
||||
icon: ?string,
|
||||
};
|
||||
|
||||
export type ChannelIconOptions = {applicationId?: ?string} & IconOptions;
|
||||
|
||||
export type SplashOptions = {
|
||||
id: string,
|
||||
splash: ?string,
|
||||
size?: number,
|
||||
};
|
||||
|
||||
export type EmojiOptions = {
|
||||
id: string,
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
uri: ?string,
|
||||
};
|
||||
|
||||
type EndpointFunction = (id: string, endpoint: string) => string;
|
||||
|
||||
let AvatarUtils;
|
||||
if (__IOS__) {
|
||||
AvatarUtils = require('./ios/AvatarUtils');
|
||||
} else if (__SDK__) {
|
||||
AvatarUtils = require('./sdk/AvatarUtils');
|
||||
} else {
|
||||
AvatarUtils = require('./web/AvatarUtils');
|
||||
}
|
||||
|
||||
function getApplicationIconURL({applicationId, icon}: AppIconOptions): ?string {
|
||||
if (icon != null) {
|
||||
if (process.env.CDN_HOST) {
|
||||
return `${location.protocol}//${process.env.CDN_HOST}/app-icons/${applicationId}/${icon}.jpg`;
|
||||
} else {
|
||||
const BASE_URL = `${location.protocol}${process.env.API_ENDPOINT}`;
|
||||
return `${BASE_URL}/oauth2/applications/${applicationId}/app-icons/${icon}.jpg`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAvatarURL(
|
||||
endpoint: EndpointFunction,
|
||||
path: string,
|
||||
id: string,
|
||||
avatar: ?string,
|
||||
size: ?number,
|
||||
format: string = 'jpg'
|
||||
): ?string {
|
||||
if (avatar != null) {
|
||||
let url;
|
||||
const CDN_HOST = process.env.CDN_HOST;
|
||||
if (CDN_HOST) {
|
||||
if (format === 'jpg' && NativeUtils.isDesktop()) {
|
||||
format = 'webp';
|
||||
} else if (format === 'jpg' && __WEB__) {
|
||||
format = 'png';
|
||||
}
|
||||
url = `${location.protocol}//${CDN_HOST}/${path}/${id}/${avatar}.${format}`;
|
||||
} else {
|
||||
url = location.protocol + process.env.API_ENDPOINT + endpoint(id, avatar);
|
||||
}
|
||||
if (size != null) {
|
||||
url = url + '?size=' + size;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function getEmojiURL({id}: EmojiOptions): string {
|
||||
if (process.env.CDN_HOST) {
|
||||
return `${location.protocol}//${process.env.CDN_HOST}/emojis/${id}.png`;
|
||||
} else {
|
||||
return location.protocol + process.env.API_ENDPOINT + Endpoints.EMOJI(id);
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserAvatarURL({id, avatar, discriminator, bot}: AvatarOptions, format: string = 'jpg'): string {
|
||||
let avatarURL;
|
||||
if (bot) {
|
||||
avatarURL = AvatarUtils.BOT_AVATARS[avatar];
|
||||
}
|
||||
avatarURL = avatarURL || getAvatarURL(Endpoints.AVATAR, 'avatars', id, avatar, AVATAR_SIZE * 2, format);
|
||||
avatarURL = avatarURL || AvatarUtils.DEFAULT_AVATARS[parseInt(discriminator) % AvatarUtils.DEFAULT_AVATARS.length];
|
||||
return avatarURL;
|
||||
}
|
||||
|
||||
type Color = string;
|
||||
|
||||
function getUserAvatarColor({discriminator}: AvatarOptions): Color {
|
||||
const index: number = parseInt(discriminator) % AvatarUtils.DEFAULT_AVATARS.length;
|
||||
switch (index) {
|
||||
case 0:
|
||||
return Colors.BRAND_PURPLE;
|
||||
case 1:
|
||||
return Colors.AVATAR_GREY;
|
||||
case 2:
|
||||
return Colors.STATUS_GREEN;
|
||||
case 3:
|
||||
return Colors.STATUS_YELLOW;
|
||||
case 4:
|
||||
return Colors.STATUS_RED;
|
||||
default:
|
||||
return Colors.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
function getGuildIconURL({id, icon}: IconOptions): ?string {
|
||||
return getAvatarURL(Endpoints.GUILD_ICON, 'icons', id, icon);
|
||||
}
|
||||
|
||||
function getGuildSplashURL({id, splash, size = 2048}: SplashOptions): ?string {
|
||||
return getAvatarURL(Endpoints.GUILD_SPLASH, 'splashes', id, splash, size);
|
||||
}
|
||||
|
||||
function getAppIconURL({id, icon}: IconOptions): ?string {
|
||||
return getAvatarURL(Endpoints.APPLICATION_ICON, 'app-icons', id, icon);
|
||||
}
|
||||
|
||||
function getChannelIconURL({id, icon, applicationId}: ChannelIconOptions): ?string {
|
||||
if (applicationId) {
|
||||
return getApplicationIconURL({applicationId, icon}) || AvatarUtils.DEFAULT_CHANNEL_ICON;
|
||||
}
|
||||
return getAvatarURL(Endpoints.CHANNEL_ICON, 'channel-icons', id, icon) || AvatarUtils.DEFAULT_CHANNEL_ICON;
|
||||
}
|
||||
|
||||
export function hasAnimatedAvatar(user: {avatar: string}): boolean {
|
||||
if (!user || !user.avatar) {
|
||||
return false;
|
||||
}
|
||||
// Using substr because it's faster than a regex on chrome
|
||||
return user.avatar.substr(0, 2) === 'a_';
|
||||
}
|
||||
|
||||
export default {
|
||||
// User
|
||||
|
||||
getUserAvatarURL,
|
||||
hasAnimatedAvatar,
|
||||
|
||||
getUserAvatarSource(user: AvatarOptions): Source {
|
||||
return {
|
||||
uri: getUserAvatarURL(user),
|
||||
};
|
||||
},
|
||||
|
||||
getUserAvatarColor,
|
||||
|
||||
// Guild
|
||||
|
||||
getGuildIconURL,
|
||||
getGuildSplashURL,
|
||||
|
||||
getChannelIconURL,
|
||||
|
||||
getEmojiURL,
|
||||
|
||||
getAppIconURL,
|
||||
|
||||
getGuildIconSource(guild: IconOptions): Source {
|
||||
return {
|
||||
uri: getGuildIconURL(guild),
|
||||
};
|
||||
},
|
||||
|
||||
getChannelIconSource(channel: ChannelIconOptions): Source {
|
||||
return {
|
||||
uri: getChannelIconURL(channel),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/AvatarUtils.js
|
38
509bba0_unpacked/discord_app/utils/ChannelUtils.js
Executable file
38
509bba0_unpacked/discord_app/utils/ChannelUtils.js
Executable file
|
@ -0,0 +1,38 @@
|
|||
/* @flow */
|
||||
|
||||
import {ChannelTypes, Permissions} from '../Constants';
|
||||
import PermissionUtils from './PermissionUtils';
|
||||
import type ChannelRecord, {PermissionOverwrite} from '../records/ChannelRecord';
|
||||
|
||||
export function permissionOverwritesForRoles(
|
||||
guildId: string,
|
||||
channelType: number,
|
||||
roles: Array<string>
|
||||
): Array<PermissionOverwrite> {
|
||||
let permissionOverwrites = [];
|
||||
if (roles.length > 0) {
|
||||
const permissions = channelType == ChannelTypes.GUILD_TEXT ? Permissions.READ_MESSAGES : Permissions.CONNECT;
|
||||
permissionOverwrites = roles.map(roleId => ({
|
||||
id: roleId,
|
||||
type: 'role',
|
||||
allow: permissions,
|
||||
deny: PermissionUtils.NONE,
|
||||
}));
|
||||
permissionOverwrites.unshift({
|
||||
id: guildId,
|
||||
type: 'role',
|
||||
allow: PermissionUtils.NONE,
|
||||
deny: permissions,
|
||||
});
|
||||
}
|
||||
return permissionOverwrites;
|
||||
}
|
||||
|
||||
export function isChannelFull(channel: ChannelRecord, voiceStates: number): boolean {
|
||||
return channel.userLimit > 0 && voiceStates >= channel.userLimit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ChannelUtils.js
|
52
509bba0_unpacked/discord_app/utils/ClipboardUtils.js
Executable file
52
509bba0_unpacked/discord_app/utils/ClipboardUtils.js
Executable file
|
@ -0,0 +1,52 @@
|
|||
/* @flow */
|
||||
|
||||
import NativeUtils from './NativeUtils';
|
||||
|
||||
export const SUPPORTS_COPY = (() => {
|
||||
if (NativeUtils.embedded) {
|
||||
return !!NativeUtils.copy;
|
||||
}
|
||||
|
||||
try {
|
||||
// $FlowFixMe
|
||||
return document.queryCommandEnabled('copy') || document.queryCommandSupported('copy');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Copies a line of text to the clipboard. Returns true if it succeeded, or false if not.
|
||||
*/
|
||||
export function copy(text: string): boolean {
|
||||
if (!SUPPORTS_COPY) return false;
|
||||
|
||||
if (NativeUtils.embedded) {
|
||||
NativeUtils.copy(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
// On the browser, we can copy something by making a text area, positioning it off screen,
|
||||
// focus and selecting it, then use the 'execCommand' api.
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'absolute';
|
||||
textArea.style.top = '-9999px';
|
||||
textArea.style.left = '-9999px';
|
||||
const body = document.body;
|
||||
if (body == null) {
|
||||
throw new Error('[Utils]ClipboardUtils.copy(): assert failed: document.body != null');
|
||||
}
|
||||
body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
const success = document.execCommand('copy');
|
||||
body.removeChild(textArea);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ClipboardUtils.js
|
93
509bba0_unpacked/discord_app/utils/ComponentDispatchUtils.js
Executable file
93
509bba0_unpacked/discord_app/utils/ComponentDispatchUtils.js
Executable file
|
@ -0,0 +1,93 @@
|
|||
/* @flow */
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
class Dispatch extends EventEmitter {
|
||||
_savedDispatches = {};
|
||||
|
||||
// Will only dispatch if components are subscribed, otherwise it will wait
|
||||
// until they subscribe to dispatch
|
||||
safeDispatch(type: string, args?: Object) {
|
||||
if (!this.hasSubscribers(type)) {
|
||||
if (!this._savedDispatches[type]) {
|
||||
this._savedDispatches[type] = [];
|
||||
}
|
||||
this._savedDispatches[type].push(args);
|
||||
return;
|
||||
}
|
||||
this.dispatch(type, args);
|
||||
}
|
||||
|
||||
dispatch(type: string, args?: Object): this {
|
||||
this.emit(type, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
dispatchToLastSubscribed(type: string, args?: Object): this {
|
||||
const listeners = this.listeners(type);
|
||||
if (listeners.length) {
|
||||
listeners[listeners.length - 1](args);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
dispatchToFirst(types: Array<string>, args?: Object) {
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
const type = types[i];
|
||||
if (this.hasSubscribers(type)) {
|
||||
this.dispatch(type, args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
hasSubscribers(type: string) {
|
||||
return this.listenerCount(type) > 0;
|
||||
}
|
||||
|
||||
_checkSavedDispatches(type: string) {
|
||||
if (this._savedDispatches[type]) {
|
||||
this._savedDispatches[type].forEach(args => this.dispatch(type, args));
|
||||
this._savedDispatches[type] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(type: string, callback: Function) {
|
||||
const listeners = this.listeners(type);
|
||||
if (listeners.indexOf(callback) >= 0) {
|
||||
console.warn('ComponentDispatch.subscribe: Attempting to add a duplicate listener', type);
|
||||
return this;
|
||||
}
|
||||
this.on(type, callback);
|
||||
this._checkSavedDispatches(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
subscribeOnce(type: string, callback: Function) {
|
||||
this.once(type, callback);
|
||||
this._checkSavedDispatches(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
unsubscribe(type: string, callback: Function) {
|
||||
this.removeListener(type, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
reset(): this {
|
||||
this.removeAllListeners();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const ComponentDispatch = new Dispatch();
|
||||
|
||||
export default {
|
||||
ComponentDispatch,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ComponentDispatchUtils.js
|
98
509bba0_unpacked/discord_app/utils/ContextMenuUtils.js
Executable file
98
509bba0_unpacked/discord_app/utils/ContextMenuUtils.js
Executable file
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import ContextMenu from '../components/common/ContextMenu';
|
||||
import NativeContextMenu from '../components/contextmenus/NativeContextMenu';
|
||||
import {ContextMenuTypes} from '../Constants';
|
||||
|
||||
function getSelectionText() {
|
||||
// from http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text
|
||||
let text = '';
|
||||
if (window.getSelection) {
|
||||
text = window.getSelection().toString();
|
||||
} else if (document.selection && document.selection.type !== 'Control') {
|
||||
text = document.selection.createRange().text;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export function contextMenuCallbackNative(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let type;
|
||||
let href;
|
||||
let src;
|
||||
const value = getSelectionText();
|
||||
|
||||
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
|
||||
type = ContextMenuTypes.NATIVE_INPUT;
|
||||
} else if (window.getComputedStyle(e.target).getPropertyValue('-webkit-user-select') == 'none') {
|
||||
// Dont contextmenu things with highlighting disabled
|
||||
return;
|
||||
} else {
|
||||
let node = e.target;
|
||||
|
||||
do {
|
||||
if (node.src != null) {
|
||||
src = node.src;
|
||||
}
|
||||
if (node.href != null) {
|
||||
href = node.href;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
} while (node != null);
|
||||
|
||||
if (src != null) {
|
||||
type = ContextMenuTypes.NATIVE_IMAGE;
|
||||
} else if (href != null) {
|
||||
type = ContextMenuTypes.NATIVE_LINK;
|
||||
} else if (value) {
|
||||
// If not a link but has highlighted value, open text context menu
|
||||
type = ContextMenuTypes.NATIVE_TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
if (type) {
|
||||
ContextMenu.openContextMenu(e, props =>
|
||||
<NativeContextMenu {...props} type={ContextMenuTypes[type]} href={href} src={src} value={value} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function contextMenuCallbackWeb(e) {
|
||||
let allow = false;
|
||||
let src;
|
||||
let href;
|
||||
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||||
allow = true;
|
||||
} else if (getSelectionText()) {
|
||||
allow = true;
|
||||
} else {
|
||||
let node = e.target;
|
||||
|
||||
do {
|
||||
if (node.src != null) {
|
||||
src = node.src;
|
||||
}
|
||||
if (node.href != null) {
|
||||
href = node.href;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
} while (node != null);
|
||||
|
||||
if (href != null || src != null) {
|
||||
allow = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allow) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ContextMenuUtils.js
|
60
509bba0_unpacked/discord_app/utils/CreditCardUtils.js
Executable file
60
509bba0_unpacked/discord_app/utils/CreditCardUtils.js
Executable file
|
@ -0,0 +1,60 @@
|
|||
import creditCardType from 'credit-card-type';
|
||||
|
||||
const DEFAULT_CARD_TYPE = {
|
||||
type: '',
|
||||
gaps: [4, 8, 12],
|
||||
lengths: [16],
|
||||
code: {
|
||||
name: 'CVV',
|
||||
size: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const CARD_NUMBER_REGEX = /[^0-9]/g;
|
||||
|
||||
function prettifyCreditCardNumber(value) {
|
||||
const arr = cleanCardNumber(value).split('');
|
||||
const type = getCreditCardType(value);
|
||||
let x = 0;
|
||||
while (x < type.gaps.length) {
|
||||
const index = type.gaps[x] + x;
|
||||
if (index < arr.length) {
|
||||
arr.splice(index, 0, ' ');
|
||||
x += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return arr.join('');
|
||||
}
|
||||
|
||||
function cleanCardNumber(cardNumber) {
|
||||
return cardNumber.replace(CARD_NUMBER_REGEX, '');
|
||||
}
|
||||
|
||||
function formatCreditCardNumber(cardNumber) {
|
||||
const cleanedCardNumber = cleanCardNumber(cardNumber);
|
||||
const type = getCreditCardType(cleanedCardNumber);
|
||||
return prettifyCreditCardNumber(cleanedCardNumber, type);
|
||||
}
|
||||
|
||||
function getCreditCardType(cardNumber) {
|
||||
const cleanedCardNumber = cleanCardNumber(cardNumber);
|
||||
if (!cleanedCardNumber) {
|
||||
return DEFAULT_CARD_TYPE;
|
||||
}
|
||||
return creditCardType(cleanedCardNumber)[0] || DEFAULT_CARD_TYPE;
|
||||
}
|
||||
|
||||
export default {
|
||||
DEFAULT_CARD_TYPE,
|
||||
prettifyCreditCardNumber,
|
||||
formatCreditCardNumber,
|
||||
getCreditCardType,
|
||||
cleanCardNumber,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/CreditCardUtils.js
|
42
509bba0_unpacked/discord_app/utils/DebugUtils.js
Executable file
42
509bba0_unpacked/discord_app/utils/DebugUtils.js
Executable file
|
@ -0,0 +1,42 @@
|
|||
import platform from 'platform';
|
||||
import NativeUtils from '../utils/NativeUtils';
|
||||
|
||||
export default {
|
||||
getIdleTime(callback) {
|
||||
NativeUtils.getIdleMilliseconds(callback);
|
||||
},
|
||||
|
||||
dump(callback) {
|
||||
let memory;
|
||||
if (performance.memory) {
|
||||
memory = {
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||
};
|
||||
}
|
||||
|
||||
const dump = {
|
||||
browser: {
|
||||
name: platform.name,
|
||||
version: platform.version,
|
||||
},
|
||||
os: {
|
||||
name: platform.os.family,
|
||||
version: platform.os.version,
|
||||
},
|
||||
memory,
|
||||
};
|
||||
|
||||
callback(dump);
|
||||
},
|
||||
|
||||
getTimeSinceNavigationStart() {
|
||||
return Date.now() - window.performance.timing.navigationStart;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/DebugUtils.js
|
57
509bba0_unpacked/discord_app/utils/DraftJSSearchUtils.js
Executable file
57
509bba0_unpacked/discord_app/utils/DraftJSSearchUtils.js
Executable file
|
@ -0,0 +1,57 @@
|
|||
import {Entity} from 'draft-js';
|
||||
import {replaceAllContent} from './DraftJSUtils';
|
||||
import {getSelectionScope as _getSelectionScope} from './SearchUtils';
|
||||
import QueryTokenizer from '../lib/QueryTokenizer';
|
||||
import {IS_SEARCH_ANSWER_TOKEN} from '../Constants';
|
||||
import type {EditorState, ContentBlock} from 'draft-js';
|
||||
|
||||
export function matchColonGroup(contentBlock: ContentBlock, callback: Function, type: string) {
|
||||
contentBlock.findEntityRanges(character => {
|
||||
const entityKey = character.getEntity();
|
||||
return entityKey !== null && Entity.get(entityKey).getType() === type;
|
||||
}, callback);
|
||||
}
|
||||
|
||||
export function generateDecorators(tokensDict: Object = {}) {
|
||||
const decorators = [];
|
||||
Object.keys(tokensDict).forEach(type => {
|
||||
const rule = tokensDict[type];
|
||||
decorators.push({
|
||||
strategy: (contentBlock, callback) => matchColonGroup(contentBlock, callback, type),
|
||||
component: rule.component,
|
||||
});
|
||||
});
|
||||
return decorators;
|
||||
}
|
||||
|
||||
export function cleanQueryTokens(tokens: Array<Token>, editorState: EditorState) {
|
||||
let queryModified = false;
|
||||
const newQuery = [];
|
||||
tokens.forEach((token, i) => {
|
||||
const nextToken = tokens[i];
|
||||
newQuery.push(token.getFullMatch());
|
||||
if (
|
||||
IS_SEARCH_ANSWER_TOKEN.test(token.type) &&
|
||||
((nextToken && nextToken.type !== QueryTokenizer.NON_TOKEN_TYPE) || !nextToken)
|
||||
) {
|
||||
queryModified = true;
|
||||
newQuery.push(' ');
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryModified) {
|
||||
return editorState;
|
||||
}
|
||||
|
||||
return replaceAllContent(newQuery.join(''), editorState);
|
||||
}
|
||||
|
||||
export function getSelectionScope(tokens: Array<Token>, editorState: EditorState): ?CursorScope {
|
||||
const {focusOffset, anchorOffset} = editorState.getSelection();
|
||||
return _getSelectionScope(tokens, focusOffset, anchorOffset);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/DraftJSSearchUtils.js
|
329
509bba0_unpacked/discord_app/utils/DraftJSUtils.js
Executable file
329
509bba0_unpacked/discord_app/utils/DraftJSUtils.js
Executable file
|
@ -0,0 +1,329 @@
|
|||
import {EditorState, Entity, SelectionState, Modifier, ContentState, CompositeDecorator} from 'draft-js';
|
||||
import keyCommandBackspaceToStartOfLine from 'draft-js/lib/keyCommandBackspaceToStartOfLine';
|
||||
import keyCommandBackspaceWord from 'draft-js/lib/keyCommandBackspaceWord';
|
||||
import keyCommandDeleteWord from 'draft-js/lib/keyCommandDeleteWord';
|
||||
import keyCommandPlainBackspace from 'draft-js/lib/keyCommandPlainBackspace';
|
||||
import keyCommandPlainDelete from 'draft-js/lib/keyCommandPlainDelete';
|
||||
import keyCommandTransposeCharacters from 'draft-js/lib/keyCommandTransposeCharacters';
|
||||
import keyCommandMoveSelectionToStartOfBlock from 'draft-js/lib/keyCommandMoveSelectionToStartOfBlock';
|
||||
import keyCommandMoveSelectionToEndOfBlock from 'draft-js/lib/keyCommandMoveSelectionToEndOfBlock';
|
||||
import getEntityKeyForSelection from 'draft-js/lib/getEntityKeyForSelection';
|
||||
import getDefaultKeyBinding from 'draft-js/lib/getDefaultKeyBinding';
|
||||
export {getDefaultKeyBinding};
|
||||
|
||||
export function createEntity(entityData, entityStart, entityEnd, editorState) {
|
||||
const entityKey = entityData ? Entity.create.call(Entity, ...entityData) : null;
|
||||
let contentState = editorState.getCurrentContent();
|
||||
const currentBlock = contentState.getFirstBlock();
|
||||
const selectionState = new SelectionState({
|
||||
anchorKey: currentBlock.getKey(),
|
||||
anchorOffset: entityStart,
|
||||
focusKey: currentBlock.getKey(),
|
||||
focusOffset: entityEnd,
|
||||
});
|
||||
|
||||
contentState = Modifier.applyEntity(contentState, selectionState, entityKey);
|
||||
return EditorState.set(editorState, {currentContent: contentState});
|
||||
}
|
||||
|
||||
export function getCollapsedSelection(text, editorState, selectionState) {
|
||||
const contentState = editorState.getCurrentContent();
|
||||
const currentBlock = contentState.getFirstBlock();
|
||||
selectionState = selectionState || editorState.getSelection();
|
||||
selectionState = selectionState.set('focusKey', currentBlock.getKey());
|
||||
selectionState = selectionState.set('anchorKey', currentBlock.getKey());
|
||||
const offset = Math.min(selectionState.getStartOffset(), selectionState.getEndOffset()) + text.length;
|
||||
selectionState = selectionState.set('anchorOffset', offset);
|
||||
selectionState = selectionState.set('focusOffset', offset);
|
||||
return selectionState;
|
||||
}
|
||||
|
||||
export function updateContent(text, editorState, anchor, focus) {
|
||||
let selectionState;
|
||||
let contentState = editorState.getCurrentContent();
|
||||
const currentBlock = contentState.getFirstBlock();
|
||||
const currentText = currentBlock.getText();
|
||||
if (typeof anchor === 'number') {
|
||||
if (anchor > currentText.length) {
|
||||
anchor = currentText.length;
|
||||
}
|
||||
if (focus > currentText.length) {
|
||||
focus = currentText.length;
|
||||
}
|
||||
selectionState = new SelectionState({
|
||||
anchorKey: currentBlock.getKey(),
|
||||
anchorOffset: anchor,
|
||||
focusKey: currentBlock.getKey(),
|
||||
focusOffset: focus || anchor,
|
||||
});
|
||||
} else {
|
||||
selectionState = editorState.getSelection();
|
||||
}
|
||||
const inlineStyle = editorState.getCurrentInlineStyle();
|
||||
const entityKey = getEntityKeyForSelection(contentState, selectionState);
|
||||
|
||||
let changeType;
|
||||
if (selectionState.isCollapsed()) {
|
||||
contentState = Modifier.insertText(contentState, selectionState, text, inlineStyle, entityKey);
|
||||
changeType = 'insert-characters';
|
||||
} else {
|
||||
contentState = Modifier.replaceText(contentState, selectionState, text, inlineStyle, entityKey);
|
||||
changeType = 'replace-characters';
|
||||
}
|
||||
|
||||
return EditorState.push(editorState, contentState, changeType);
|
||||
}
|
||||
|
||||
export function deleteContent(type, editorState) {
|
||||
switch (type) {
|
||||
case 'delete':
|
||||
return keyCommandPlainDelete(editorState);
|
||||
case 'delete-word':
|
||||
return keyCommandDeleteWord(editorState);
|
||||
case 'backspace':
|
||||
return keyCommandPlainBackspace(editorState);
|
||||
case 'backspace-word':
|
||||
return keyCommandBackspaceWord(editorState);
|
||||
case 'backspace-to-start-of-line':
|
||||
return keyCommandBackspaceToStartOfLine(editorState);
|
||||
default:
|
||||
return editorState;
|
||||
}
|
||||
}
|
||||
|
||||
export function miscCommand(command, editorState) {
|
||||
switch (command) {
|
||||
case 'transpose-characters':
|
||||
return keyCommandTransposeCharacters(editorState);
|
||||
case 'move-selection-to-start-of-block':
|
||||
return keyCommandMoveSelectionToStartOfBlock(editorState);
|
||||
case 'move-selection-to-end-of-block':
|
||||
return keyCommandMoveSelectionToEndOfBlock(editorState);
|
||||
default:
|
||||
return editorState;
|
||||
}
|
||||
}
|
||||
|
||||
export function adaptSelection(selectionState, contentBlock) {
|
||||
selectionState = selectionState.set('focusKey', contentBlock.getKey());
|
||||
selectionState = selectionState.set('anchorKey', contentBlock.getKey());
|
||||
const offset = selectionState.getEndOffset();
|
||||
selectionState = selectionState.set('anchorOffset', offset);
|
||||
selectionState = selectionState.set('focusOffset', offset);
|
||||
return selectionState;
|
||||
}
|
||||
|
||||
export function getFirstTextBlock(editorState) {
|
||||
return editorState.getCurrentContent().getFirstBlock().getText();
|
||||
}
|
||||
|
||||
export function applyTokensAsEntities(tokens, editorState, tokenTypes = {}) {
|
||||
const contentState = editorState.getCurrentContent();
|
||||
const currentBlock = contentState.getFirstBlock();
|
||||
const currentText = currentBlock.getText();
|
||||
|
||||
const entities = [];
|
||||
currentBlock.findEntityRanges(
|
||||
character => {
|
||||
return character.getEntity() !== null;
|
||||
},
|
||||
(start, end) => {
|
||||
const type = Entity.get(currentBlock.getEntityAt(start)).getType();
|
||||
const text = currentText.substring(start, end);
|
||||
entities.push({
|
||||
processed: false,
|
||||
type,
|
||||
start,
|
||||
end,
|
||||
text,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
tokens.forEach(token => {
|
||||
let entityExists = false;
|
||||
entities.forEach(entity => {
|
||||
const {type, start, end} = token;
|
||||
const text = token.getFullMatch();
|
||||
// If we have already processed this entity,
|
||||
// there is no need to check on it again
|
||||
if (entity.processed) {
|
||||
return;
|
||||
}
|
||||
// Determine if entity already exists and is an exact match
|
||||
if (entity.type === type && entity.start === start && entity.text === text) {
|
||||
entity.processed = true;
|
||||
entityExists = true;
|
||||
} else if ((start >= entity.start && start < entity.end) || (end > entity.start && end <= entity.end)) {
|
||||
// Determine if there is a conflicting entity, if so, remove it
|
||||
entity.processed = true;
|
||||
editorState = createEntity(null, entity.start, entity.end, editorState);
|
||||
}
|
||||
});
|
||||
|
||||
if (entityExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenType = tokenTypes[token.type];
|
||||
editorState = createEntity(
|
||||
[token.type, tokenType && tokenType.mutable ? 'MUTABLE' : 'IMMUTABLE', {token}],
|
||||
token.start,
|
||||
token.end,
|
||||
editorState
|
||||
);
|
||||
});
|
||||
|
||||
// Clean out any entities that were not processed
|
||||
entities.forEach(entity => {
|
||||
if (!entity.processed) {
|
||||
editorState = createEntity(null, entity.start, entity.end, editorState);
|
||||
}
|
||||
});
|
||||
|
||||
return editorState;
|
||||
}
|
||||
|
||||
export function getSelectionScope(tokens, editorState) {
|
||||
const {focusOffset, anchorOffset} = editorState.getSelection();
|
||||
let previousToken;
|
||||
let nextToken;
|
||||
const currentToken = tokens.find((token, index) => {
|
||||
if (
|
||||
focusOffset >= token.start &&
|
||||
focusOffset <= token.end &&
|
||||
anchorOffset >= token.start &&
|
||||
anchorOffset <= token.end
|
||||
) {
|
||||
if (tokens[index + 1]) {
|
||||
nextToken = tokens[index + 1];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
previousToken = token;
|
||||
return false;
|
||||
});
|
||||
|
||||
// If we can't find a currentToken it means we have a selection that breaks
|
||||
// outside a token and therefore can't be properly scoped
|
||||
if (!currentToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
previousToken,
|
||||
currentToken,
|
||||
nextToken,
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyEditorState(decorators) {
|
||||
return EditorState.createEmpty(new CompositeDecorator(decorators));
|
||||
}
|
||||
|
||||
export function clearContent(editorState) {
|
||||
return EditorState.push(editorState, ContentState.createFromText(''));
|
||||
}
|
||||
|
||||
export function replaceAllContent(text, editorState) {
|
||||
const currentText = getFirstTextBlock(editorState);
|
||||
return updateContent(text, editorState, 0, currentText.length);
|
||||
}
|
||||
|
||||
export function appendSpace(editorState) {
|
||||
const currentText = getFirstTextBlock(editorState);
|
||||
return updateContent(' ', editorState, currentText.length);
|
||||
}
|
||||
|
||||
export function setCollapsedSelection(offset, editorState) {
|
||||
let selectionState = editorState.getSelection();
|
||||
selectionState = selectionState.set('focusOffset', offset);
|
||||
selectionState = selectionState.set('anchorOffset', offset);
|
||||
return EditorState.forceSelection(editorState, selectionState);
|
||||
}
|
||||
|
||||
export function setCollapsedEndSelection(editorState) {
|
||||
const text = editorState.getCurrentContent().getFirstBlock().getText();
|
||||
return setCollapsedSelection(text.length, editorState);
|
||||
}
|
||||
|
||||
export function setCollapsedStartSelection(editorState) {
|
||||
return setCollapsedSelection(0, editorState);
|
||||
}
|
||||
|
||||
export function setToStartSelection(editorState) {
|
||||
let selectionState = editorState.getSelection();
|
||||
selectionState = selectionState.set('focusOffset', 0);
|
||||
selectionState = selectionState.set('isBackward', true);
|
||||
return EditorState.forceSelection(editorState, selectionState);
|
||||
}
|
||||
|
||||
export function setToEndSelection(editorState) {
|
||||
const currentText = getFirstTextBlock(editorState);
|
||||
let selectionState = editorState.getSelection();
|
||||
selectionState = selectionState.set('focusOffset', currentText.length);
|
||||
selectionState = selectionState.set('isBackward', false);
|
||||
return EditorState.forceSelection(editorState, selectionState);
|
||||
}
|
||||
|
||||
export function truncateContent(editorState, maxLength = 512) {
|
||||
const query = getFirstTextBlock(editorState);
|
||||
if (query.length > maxLength) {
|
||||
let selectionState = editorState.getSelection();
|
||||
editorState = updateContent('', editorState, maxLength, query.length);
|
||||
// Adapt the old selection selection after truncation
|
||||
if (selectionState.getAnchorOffset() > maxLength) {
|
||||
selectionState = selectionState.set('anchorOffset', maxLength);
|
||||
}
|
||||
if (selectionState.getFocusOffset() > maxLength) {
|
||||
selectionState = selectionState.set('focusOffset', maxLength);
|
||||
}
|
||||
editorState = EditorState.forceSelection(editorState, selectionState);
|
||||
}
|
||||
return editorState;
|
||||
}
|
||||
|
||||
export function scrollCursorIntoView(editorEl) {
|
||||
const selection = window.getSelection();
|
||||
// We should ONLY update Caret, i.e. Collapsed selections
|
||||
if (!selection || selection.type !== 'Caret' || !editorEl) {
|
||||
return;
|
||||
}
|
||||
// We should only act on selections within our editor
|
||||
const range = selection.getRangeAt(0);
|
||||
if (!isContainedWithin(range.commonAncestorContainer, editorEl)) {
|
||||
return;
|
||||
}
|
||||
const rangeRect = range.getClientRects()[0];
|
||||
const editorRect = editorEl.getClientRects()[0];
|
||||
if (!rangeRect || !editorRect) {
|
||||
return;
|
||||
}
|
||||
// Calculate position of caret from getClientRects and ensure it's visible
|
||||
const left = rangeRect.left - editorRect.left;
|
||||
const scrollLeft = left + editorEl.scrollLeft;
|
||||
if (scrollLeft < editorEl.scrollLeft) {
|
||||
editorEl.scrollLeft = scrollLeft - 10;
|
||||
} else if (scrollLeft > editorEl.scrollLeft + editorEl.offsetWidth) {
|
||||
editorEl.scrollLeft = scrollLeft - editorEl.offsetWidth + 3;
|
||||
}
|
||||
}
|
||||
|
||||
function isContainedWithin(node, parentNode) {
|
||||
while (node) {
|
||||
if (node === parentNode) {
|
||||
return true;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isEmpty(editorState) {
|
||||
return getFirstTextBlock(editorState).length === 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/DraftJSUtils.js
|
94
509bba0_unpacked/discord_app/utils/DragAndDropUtils.js
Executable file
94
509bba0_unpacked/discord_app/utils/DragAndDropUtils.js
Executable file
|
@ -0,0 +1,94 @@
|
|||
/* @flow */
|
||||
|
||||
import lodash from 'lodash';
|
||||
|
||||
type PositionUpdate = {
|
||||
id: string,
|
||||
position: number,
|
||||
};
|
||||
|
||||
function calculatePositionDeltas<T>(
|
||||
oldOrdering: Array<T>,
|
||||
newOrdering: Array<T>,
|
||||
idGetter: (obj: T) => string,
|
||||
existingPositionGetter: (obj: T) => number,
|
||||
ascending: boolean = true
|
||||
): Array<PositionUpdate> {
|
||||
const len = newOrdering.length;
|
||||
if (oldOrdering.length !== len) {
|
||||
console.warn('Arrays are not of the same length!', oldOrdering, newOrdering);
|
||||
return [];
|
||||
}
|
||||
|
||||
const oldIds = oldOrdering.map(idGetter).sort().join(':');
|
||||
const newIds = newOrdering.map(idGetter).sort().join(':');
|
||||
if (oldIds !== newIds) {
|
||||
console.warn('Object IDs in the old ordering and the new ordering are not the same.', oldIds, newIds);
|
||||
return [];
|
||||
}
|
||||
|
||||
const oldObjectsById = {};
|
||||
|
||||
// Build id to position mapping
|
||||
for (let i = 0; i < len; i++) {
|
||||
oldObjectsById[idGetter(oldOrdering[i])] = existingPositionGetter(oldOrdering[i]);
|
||||
}
|
||||
|
||||
// Compute position deltas required.
|
||||
const positionDeltas = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
const objectId = idGetter(newOrdering[i]);
|
||||
const oldPosition = oldObjectsById[objectId];
|
||||
const newPosition = ascending ? i : len - i;
|
||||
|
||||
if (oldPosition !== newPosition || existingPositionGetter(newOrdering[i]) !== newPosition) {
|
||||
positionDeltas.push({
|
||||
id: objectId,
|
||||
position: newPosition,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!ascending) {
|
||||
positionDeltas.reverse();
|
||||
}
|
||||
|
||||
return positionDeltas;
|
||||
}
|
||||
|
||||
function moveItemFromTo<T>(objectArray: Array<T>, fromPosition: number, toPosition: number): Array<T> {
|
||||
const itemMoved = objectArray[fromPosition];
|
||||
|
||||
const newArray = [...objectArray];
|
||||
newArray.splice(fromPosition, 1);
|
||||
newArray.splice(toPosition, 0, itemMoved);
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
function getPositionUpdates<T>(
|
||||
objectArray: Array<T> | {[key: string]: T},
|
||||
fromPosition: number,
|
||||
toPosition: number,
|
||||
idGetter: (obj: T) => string,
|
||||
existingPositionGetter: (obj: T) => number,
|
||||
ascending: boolean = true
|
||||
): Array<PositionUpdate> {
|
||||
if (!Array.isArray(objectArray)) {
|
||||
objectArray = lodash.values(objectArray);
|
||||
}
|
||||
|
||||
const newArray = moveItemFromTo(objectArray, fromPosition, toPosition);
|
||||
return calculatePositionDeltas(objectArray, newArray, idGetter, existingPositionGetter, ascending);
|
||||
}
|
||||
|
||||
export default {
|
||||
moveItemFromTo,
|
||||
calculatePositionDeltas,
|
||||
getPositionUpdates,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/DragAndDropUtils.js
|
86
509bba0_unpacked/discord_app/utils/EmojiUtils.js
Executable file
86
509bba0_unpacked/discord_app/utils/EmojiUtils.js
Executable file
|
@ -0,0 +1,86 @@
|
|||
/* @flow */
|
||||
|
||||
import UserStore from '../stores/UserStore';
|
||||
import type ChannelRecord from '../records/ChannelRecord';
|
||||
import PermissionUtils from './PermissionUtils';
|
||||
import {Permissions, ChannelTypes} from '../Constants';
|
||||
|
||||
let EmojiUtils = {};
|
||||
if (__SDK__) {
|
||||
EmojiUtils = require('./sdk/EmojiUtils');
|
||||
} else if (__WEB__) {
|
||||
EmojiUtils = require('./web/EmojiUtils');
|
||||
}
|
||||
|
||||
type Emoji = {
|
||||
managed?: boolean,
|
||||
guildId?: number,
|
||||
};
|
||||
|
||||
export default {
|
||||
getURL() {
|
||||
return '';
|
||||
},
|
||||
|
||||
...EmojiUtils,
|
||||
|
||||
isEmojiFiltered(emoji: Emoji, channel: ChannelRecord) {
|
||||
const user = UserStore.getCurrentUser();
|
||||
|
||||
// everyone can select non-custom emojis regardless of where
|
||||
if (!emoji.guildId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// a person can select all custom emojis in groupdms and dms
|
||||
if (channel.type !== ChannelTypes.GUILD_TEXT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we want to show any emojis that the user can use, including ones requiring premium
|
||||
// this means we want to only hide emojis which the user can never use
|
||||
// (i.e. filter out external emojis when the user has no external emoji permission)
|
||||
const emojiInGuild = emoji.guildId === channel.getGuildId();
|
||||
const externalEmojiPermission = PermissionUtils.can(Permissions.USE_EXTERNAL_EMOJIS, user, channel);
|
||||
return !emojiInGuild && !externalEmojiPermission;
|
||||
},
|
||||
|
||||
isEmojiDisabled(emoji: Emoji, channel: ChannelRecord) {
|
||||
const user = UserStore.getCurrentUser();
|
||||
|
||||
// everyone can use non-custom emojis regardless of where
|
||||
if (!emoji.guildId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (channel.type === ChannelTypes.GUILD_TEXT) {
|
||||
// allows emoji use in a guild channel for two cases:
|
||||
// 1. emoji belongs to the guild
|
||||
// 2. emoji is external AND user has External Emoji permission
|
||||
// AND
|
||||
// User is premium
|
||||
// OR
|
||||
// Emoji is managed (BetterTTV/Beam) and can be used cross guild
|
||||
const emojiInGuild = emoji.guildId === channel.getGuildId();
|
||||
|
||||
const externalEmojiPermission = PermissionUtils.can(Permissions.USE_EXTERNAL_EMOJIS, user, channel);
|
||||
const canUseEmoji = emoji.managed || user.premium;
|
||||
const externallyUsableEmoji = !emojiInGuild && externalEmojiPermission && canUseEmoji;
|
||||
|
||||
return !emojiInGuild && !externallyUsableEmoji;
|
||||
}
|
||||
|
||||
// if a person is premium they can use all custom emojis in groupdms and dms
|
||||
if (user.premium) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if an emote is a custom emoji and it's not managed (i.e. not BetterTTV/Beam) they cannot use it
|
||||
return !emoji.managed;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/EmojiUtils.js
|
85
509bba0_unpacked/discord_app/utils/ErrorUtils.js
Executable file
85
509bba0_unpacked/discord_app/utils/ErrorUtils.js
Executable file
|
@ -0,0 +1,85 @@
|
|||
import Raven from 'raven-js';
|
||||
|
||||
const {DSN, updateNativeReporter, crash} = __IOS__
|
||||
? require('./ios/ErrorUtils')
|
||||
: __SDK__ ? require('./sdk/ErrorUtils') : require('./web/ErrorUtils');
|
||||
|
||||
Raven.config(DSN, {
|
||||
environment: process.env.RELEASE_CHANNEL,
|
||||
release: process.env.BUILD_NUMBER,
|
||||
ignoreErrors: [
|
||||
'EADDRINUSE',
|
||||
'BetterDiscord',
|
||||
'jQuery',
|
||||
'localStorage',
|
||||
'has already been declared',
|
||||
// This is a benign error triggered by dragging elements between windows, fixing would require forking react-dnd.
|
||||
// https://sentry.io/discord/discord-web/issues/224658494/
|
||||
'Cannot call hover while not dragging.',
|
||||
// This usually means React is an inconsistent state caused by an earlier error.
|
||||
'getHostNode',
|
||||
// BS plugins
|
||||
'setupCSS',
|
||||
// Ignore remote object errors
|
||||
'on missing remote object',
|
||||
],
|
||||
}).install();
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Set user.
|
||||
*/
|
||||
setUser(id: string, username: string, email: ?string) {
|
||||
const user = {id, username, email};
|
||||
Raven.setUserContext(user);
|
||||
updateNativeReporter(user);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear user.
|
||||
*/
|
||||
clearUser() {
|
||||
Raven.setUserContext();
|
||||
updateNativeReporter();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add tags to be sent with payload.
|
||||
*/
|
||||
setTags(tags: {[key: string]: string}) {
|
||||
Raven.setTagsContext(tags);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add extra data to be sent with payload.
|
||||
*/
|
||||
setExtra(extra: {[key: string]: any}) {
|
||||
Raven.setExtraContext(extra);
|
||||
},
|
||||
|
||||
/**
|
||||
* Capture an exception and sent it to the error tracker.
|
||||
*/
|
||||
captureException(e: Error) {
|
||||
Raven.captureException(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Capture a message and sent it to the error tracker.
|
||||
*/
|
||||
captureMessage(message: string) {
|
||||
Raven.captureMessage(message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Force crash the app, used for testing.
|
||||
*/
|
||||
crash() {
|
||||
crash();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ErrorUtils.js
|
63
509bba0_unpacked/discord_app/utils/ExperimentUtils.js
Executable file
63
509bba0_unpacked/discord_app/utils/ExperimentUtils.js
Executable file
|
@ -0,0 +1,63 @@
|
|||
/* @flow */
|
||||
|
||||
import lodash from 'lodash';
|
||||
import {ExperimentTypes} from '../Constants';
|
||||
import ExperimentActionCreators from '../actions/ExperimentActionCreators';
|
||||
import type {ExperimentDescriptor, ExperimentBucket} from '../flow/Client';
|
||||
import ExperimentStore from '../stores/ExperimentStore';
|
||||
|
||||
function getFirstEligibleUserExperiment(names: Array<string>): ?ExperimentDescriptor {
|
||||
for (const name of names) {
|
||||
const experimentDescriptor = ExperimentStore.getEligibleExperiment(name);
|
||||
if (experimentDescriptor != null) {
|
||||
return experimentDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function triggerFirstEligibleUserExperiment(names: Array<string>): ?ExperimentDescriptor {
|
||||
const eligibleExperiment = getFirstEligibleUserExperiment(names);
|
||||
if (eligibleExperiment) {
|
||||
ExperimentActionCreators.trigger(eligibleExperiment);
|
||||
return eligibleExperiment;
|
||||
}
|
||||
}
|
||||
|
||||
function isInExperimentBucket(experiment: string, experimentBucket: ExperimentBucket) {
|
||||
const bucket = ExperimentStore.getExperimentBucket(experiment);
|
||||
return bucket === experimentBucket;
|
||||
}
|
||||
|
||||
function experimentDescriptorEquals(a: ?ExperimentDescriptor, b: ?ExperimentDescriptor) {
|
||||
if (a == null && b == null) return true;
|
||||
if (a === b) return true;
|
||||
|
||||
// Quick equalities.
|
||||
if (a == null && b != null) return false;
|
||||
if (a != null && b == null) return false;
|
||||
|
||||
if (a != null && b != null) {
|
||||
if (a.type !== b.type) return false;
|
||||
if (a.bucket !== b.bucket) return false;
|
||||
if (a.revision !== b.revision) return false;
|
||||
if (a.name !== b.name) return false;
|
||||
if (a.type === ExperimentTypes.USER && b.type === ExperimentTypes.USER) {
|
||||
return lodash.isEqual(a.context, b.context);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
getFirstEligibleUserExperiment,
|
||||
isInExperimentBucket,
|
||||
experimentDescriptorEquals,
|
||||
triggerFirstEligibleUserExperiment,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ExperimentUtils.js
|
91
509bba0_unpacked/discord_app/utils/FileUtils.js
Executable file
91
509bba0_unpacked/discord_app/utils/FileUtils.js
Executable file
|
@ -0,0 +1,91 @@
|
|||
/* @flow */
|
||||
|
||||
import lodash from 'lodash';
|
||||
import humanize from 'humanize';
|
||||
import {MAX_ATTACHMENT_SIZE, MAX_PREMIUM_ATTACHMENT_SIZE} from '../Constants';
|
||||
import UserStore from '../stores/UserStore';
|
||||
|
||||
export function getFilename(file: File): any {
|
||||
if (file.overrideName) {
|
||||
return file.overrideName;
|
||||
}
|
||||
if (file.name) {
|
||||
return file.name;
|
||||
}
|
||||
if (file.filename) {
|
||||
return file.filename;
|
||||
}
|
||||
|
||||
const fileName = 'unknown';
|
||||
|
||||
switch (file.type) {
|
||||
case 'image/png':
|
||||
return fileName + '.png';
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const extensions = [
|
||||
{reType: /^image\/vnd.adobe.photoshop/, klass: 'photoshop'},
|
||||
{reType: /^image\//, klass: 'image'},
|
||||
{reType: /^video\//, klass: 'video'},
|
||||
{reName: /\.pdf$/, klass: 'acrobat'},
|
||||
{reName: /\.ae/, klass: 'ae'},
|
||||
{reName: /\.sketch$/, klass: 'sketch'},
|
||||
{reName: /\.ai$/, klass: 'ai'},
|
||||
{reName: /\.(?:rar|zip|7z|tar|tar\.gz)$/, klass: 'archive'},
|
||||
{
|
||||
reName: /\.(?:c\+\+|cpp|cc|c|h|hpp|mm|m|json|js|rb|rake|py|asm|fs|pyc|dtd|cgi|bat|rss|java|graphml|idb|lua|o|gml|prl|sls|conf|cmake|make|sln|vbe|cxx|wbf|vbs|r|wml|php|bash|applescript|fcgi|yaml|ex|exs|sh|ml|actionscript)$/,
|
||||
klass: 'code',
|
||||
}, // eslint-disable-line
|
||||
{reName: /\.(?:txt|rtf|doc|docx|md|pages|ppt|pptx|pptm|key|log)$/, klass: 'document'},
|
||||
{reName: /\.(?:xls|xlsx|numbers|csv)$/, klass: 'spreadsheet'},
|
||||
{reName: /\.(?:html|xhtml|htm|js|xml|xls|xsd|css|styl)$/, klass: 'webcode'},
|
||||
];
|
||||
|
||||
export function classifyFile(file: File): string {
|
||||
return classifyFileName(file.name, file.type);
|
||||
}
|
||||
|
||||
export function classifyFileName(name: string, type: string): string {
|
||||
name = name ? name.toLowerCase() : '';
|
||||
|
||||
const extension = lodash.find(extensions, e => {
|
||||
if (e.reType && type) {
|
||||
return e.reType.test(type);
|
||||
} else if (e.reName && name) {
|
||||
return e.reName.test(name);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return extension ? extension.klass : 'unknown';
|
||||
}
|
||||
|
||||
export function sizeString(size: number): string {
|
||||
return humanize.filesize(size);
|
||||
}
|
||||
|
||||
export function maxFileSize(): number {
|
||||
const user = UserStore.getCurrentUser();
|
||||
return user && user.premium ? MAX_PREMIUM_ATTACHMENT_SIZE : MAX_ATTACHMENT_SIZE;
|
||||
}
|
||||
|
||||
export function fileTooLarge(file: File, maxSize: number): boolean {
|
||||
if (maxSize == null) {
|
||||
maxSize = maxFileSize();
|
||||
}
|
||||
return file.size > maxSize;
|
||||
}
|
||||
|
||||
export function anyFileTooLarge(files: Array<File>): boolean {
|
||||
const maxSize = maxFileSize();
|
||||
return lodash.some(files, f => fileTooLarge(f, maxSize));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/FileUtils.js
|
10
509bba0_unpacked/discord_app/utils/FingerprintUtils.js
Executable file
10
509bba0_unpacked/discord_app/utils/FingerprintUtils.js
Executable file
|
@ -0,0 +1,10 @@
|
|||
/* @flow */
|
||||
|
||||
export function extractId(fingerprint: string) {
|
||||
return fingerprint.split('.')[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/FingerprintUtils.js
|
52
509bba0_unpacked/discord_app/utils/FriendsUtils.js
Executable file
52
509bba0_unpacked/discord_app/utils/FriendsUtils.js
Executable file
|
@ -0,0 +1,52 @@
|
|||
/* @flow */
|
||||
|
||||
import i18n from '../i18n';
|
||||
import {StatusTypes, AbortCodes} from '../Constants';
|
||||
|
||||
export function validateDiscordTag(value: string): ?string {
|
||||
if (/^(.+?@.+?\..+?|.+?#\d{4})$/.test(value)) {
|
||||
return null;
|
||||
} else if (/^DiscordTag/i.test(value)) {
|
||||
return i18n.Messages.ADD_FRIEND_ERROR_DISCORD_TAG_USERNAME;
|
||||
} else if (/^\d+$/.test(value)) {
|
||||
return i18n.Messages.ADD_FRIEND_ERROR_NUMBERS_ONLY;
|
||||
} else if (value.length > 0 && value.indexOf('#') === -1) {
|
||||
return i18n.Messages.ADD_FRIEND_ERROR_USERNAME_ONLY.format({username: value});
|
||||
}
|
||||
return i18n.Messages.ADD_FRIEND_ERROR_OTHER;
|
||||
}
|
||||
|
||||
export function humanizeAbortCode(code: number, discordTag: string): string {
|
||||
switch (code) {
|
||||
case AbortCodes.RELATIONSHIP_INCOMING_DISABLED:
|
||||
return i18n.Messages.ADD_FRIEND_ERROR_INVALID_DISCORD_TAG.format({discordTag});
|
||||
case AbortCodes.RELATIONSHIP_INCOMING_BLOCKED:
|
||||
case AbortCodes.RELATIONSHIP_INVALID_SELF:
|
||||
case AbortCodes.RELATIONSHIP_INVALUD_USER_BOT:
|
||||
case AbortCodes.RELATIONSHIP_INVALID_DISCORD_TAG:
|
||||
default:
|
||||
return i18n.Messages.ADD_FRIEND_ERROR_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusText(status: string): string {
|
||||
switch (status) {
|
||||
case StatusTypes.ONLINE:
|
||||
return i18n.Messages.STATUS_ONLINE;
|
||||
case StatusTypes.OFFLINE:
|
||||
case StatusTypes.INVISIBLE:
|
||||
return i18n.Messages.STATUS_OFFLINE;
|
||||
case StatusTypes.IDLE:
|
||||
return i18n.Messages.STATUS_IDLE;
|
||||
case StatusTypes.DND:
|
||||
return i18n.Messages.STATUS_DND;
|
||||
case StatusTypes.UNKNOWN:
|
||||
default:
|
||||
return i18n.Messages.STATUS_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/FriendsUtils.js
|
47
509bba0_unpacked/discord_app/utils/GuildUtils.js
Executable file
47
509bba0_unpacked/discord_app/utils/GuildUtils.js
Executable file
|
@ -0,0 +1,47 @@
|
|||
import GuildActionCreators from '../actions/GuildActionCreators';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import GuildSyncStore from '../stores/GuildSyncStore';
|
||||
import lodash from 'lodash';
|
||||
|
||||
const queryCache: {[key: ?string]: Set} = {};
|
||||
|
||||
let requestMembersDebouncedId = null;
|
||||
function requestMembersDebounced(guildId, query, limit) {
|
||||
clearTimeout(requestMembersDebouncedId);
|
||||
query = query.toLocaleLowerCase();
|
||||
requestMembersDebouncedId = setTimeout(() => {
|
||||
if (guildId == null) {
|
||||
const guildIds = lodash(GuildStore.getGuilds())
|
||||
.filter(guild => guild.large || !GuildSyncStore.isSynced(guild.id))
|
||||
.map(guild => guild.id)
|
||||
.value();
|
||||
if (guildIds.length > 0) {
|
||||
GuildActionCreators.requestMembers(guildIds, query, limit);
|
||||
}
|
||||
} else {
|
||||
const guild = GuildStore.getGuild(guildId);
|
||||
if (guild != null && (guild.large || !GuildSyncStore.isSynced(guild.id))) {
|
||||
GuildActionCreators.requestMembers(guild.id, query, limit);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
export default {
|
||||
requestMembers(guildId, query, limit = 10) {
|
||||
const queries = (queryCache[guildId] = queryCache[guildId] || new Set());
|
||||
if (queries.has(query) === false) {
|
||||
queries.add(query);
|
||||
requestMembersDebounced(guildId, query, limit);
|
||||
}
|
||||
},
|
||||
|
||||
getAcronym(name) {
|
||||
return name != null ? name.replace(/\w+/g, match => match[0]).replace(/\s/g, '') : '';
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/GuildUtils.js
|
124
509bba0_unpacked/discord_app/utils/HTTPUtils.js
Executable file
124
509bba0_unpacked/discord_app/utils/HTTPUtils.js
Executable file
|
@ -0,0 +1,124 @@
|
|||
/* @flow */
|
||||
|
||||
import superagent from 'superagent';
|
||||
import Backoff from '../lib/Backoff';
|
||||
import AnalyticsUtils from './AnalyticsUtils';
|
||||
|
||||
type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'del';
|
||||
|
||||
type HTTPHeaders = {[key: string]: string};
|
||||
|
||||
type HTTPRequest = {
|
||||
url: string,
|
||||
query?: {[key: string]: any} | string,
|
||||
body?: any,
|
||||
headers?: HTTPHeaders,
|
||||
backoff?: Backoff,
|
||||
retried?: number,
|
||||
retries?: number,
|
||||
};
|
||||
|
||||
type HTTPResponse = {
|
||||
status: number,
|
||||
headers: HTTPHeaders,
|
||||
body: any,
|
||||
};
|
||||
|
||||
type HTTPError = {
|
||||
err: Error,
|
||||
};
|
||||
|
||||
type HTTPResponseCallback = (res: ({ok: boolean} & HTTPResponse) | ({ok: boolean} & HTTPError)) => void;
|
||||
|
||||
function sendRequest(method: HTTPMethod, opts: HTTPRequest, resolve, reject, callback) {
|
||||
const r = superagent[method](opts.url);
|
||||
if (opts.query) {
|
||||
r.query(opts.query);
|
||||
}
|
||||
if (opts.body) {
|
||||
r.send(opts.body);
|
||||
}
|
||||
if (opts.headers) {
|
||||
r.set(opts.headers);
|
||||
}
|
||||
if (opts.context) {
|
||||
const contextProperties = AnalyticsUtils.encodeProperties(opts.context);
|
||||
if (contextProperties != null) {
|
||||
r.set('X-Context-Properties', contextProperties);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.retried) {
|
||||
r.set('X-Failed-Requests', `${opts.retried}`);
|
||||
}
|
||||
|
||||
const retry = () => {
|
||||
opts.backoff = opts.backoff || new Backoff();
|
||||
opts.retried = (opts.retried || 0) + 1;
|
||||
opts.backoff.fail(() => sendRequest(method, opts, resolve, reject, callback));
|
||||
};
|
||||
|
||||
r.on('error', err => {
|
||||
if (opts.retries != null && opts.retries-- > 0) {
|
||||
retry();
|
||||
} else {
|
||||
reject(err);
|
||||
if (callback != null) {
|
||||
callback({ok: false, err});
|
||||
}
|
||||
}
|
||||
});
|
||||
r.end(res => {
|
||||
if (opts.retries != null && opts.retries-- > 0 && res.status >= 500) {
|
||||
return retry();
|
||||
}
|
||||
|
||||
const newRes = {
|
||||
headers: res.headers,
|
||||
body: res.body,
|
||||
status: res.status,
|
||||
};
|
||||
|
||||
if (res.ok) {
|
||||
resolve(newRes);
|
||||
} else {
|
||||
reject(newRes);
|
||||
}
|
||||
if (callback != null) {
|
||||
callback({ok: res.ok, ...newRes});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeRequest(
|
||||
method: HTTPMethod,
|
||||
opts: string | HTTPRequest,
|
||||
callback?: HTTPResponseCallback
|
||||
): Promise<HTTPResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof opts === 'string') {
|
||||
opts = {url: opts};
|
||||
}
|
||||
sendRequest(method, opts, resolve, reject, callback);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
get: makeRequest.bind(null, 'get'),
|
||||
post: makeRequest.bind(null, 'post'),
|
||||
put: makeRequest.bind(null, 'put'),
|
||||
patch: makeRequest.bind(null, 'patch'),
|
||||
delete: makeRequest.bind(null, 'del'),
|
||||
getAPIBaseURL(version: boolean = true) {
|
||||
return (
|
||||
(process.env.API_PROTOCOL || location.protocol) +
|
||||
process.env.API_ENDPOINT +
|
||||
(version ? `/v${process.env.API_VERSION}` : '')
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/HTTPUtils.js
|
43
509bba0_unpacked/discord_app/utils/HelpdeskUtils.js
Executable file
43
509bba0_unpacked/discord_app/utils/HelpdeskUtils.js
Executable file
|
@ -0,0 +1,43 @@
|
|||
import UserSettingsStore from '../stores/UserSettingsStore';
|
||||
|
||||
const SUPPORT_LOCATION = 'https://support.discordapp.com';
|
||||
|
||||
function wrapURL(url) {
|
||||
return SUPPORT_LOCATION + url;
|
||||
}
|
||||
|
||||
function getLocale() {
|
||||
return UserSettingsStore.locale.toLowerCase();
|
||||
}
|
||||
|
||||
export default {
|
||||
getArticleURL(articleId) {
|
||||
return wrapURL(`/hc/${getLocale()}/articles/${articleId}`);
|
||||
},
|
||||
|
||||
getTwitterURL() {
|
||||
return 'http://www.twitter.com/discordapp';
|
||||
},
|
||||
|
||||
getCommunityURL() {
|
||||
return wrapURL(`/hc/${getLocale()}`);
|
||||
},
|
||||
|
||||
getSubmitRequestURL() {
|
||||
return wrapURL(`/hc/${getLocale()}/requests/new`);
|
||||
},
|
||||
|
||||
getSearchURL(query) {
|
||||
const escapedQuery = encodeURIComponent(query);
|
||||
return wrapURL(`/hc/${getLocale()}/search?utf8=%E2%9C%93&query=${escapedQuery}&commit=Search`);
|
||||
},
|
||||
|
||||
getFeaturedArticlesJsonURL() {
|
||||
return wrapURL('/api/v2/help_center/articles.json?label_names=featured');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/HelpdeskUtils.js
|
74
509bba0_unpacked/discord_app/utils/ImageLoaderUtils.js
Executable file
74
509bba0_unpacked/discord_app/utils/ImageLoaderUtils.js
Executable file
|
@ -0,0 +1,74 @@
|
|||
/* @flow */
|
||||
|
||||
import LRU from 'lru-cache';
|
||||
|
||||
const images = new LRU({
|
||||
max: 1000,
|
||||
});
|
||||
|
||||
type ImageData = {
|
||||
url: string,
|
||||
loaded: boolean,
|
||||
callbacks?: Set,
|
||||
width?: number,
|
||||
height?: number,
|
||||
};
|
||||
|
||||
function loadImageAsset(imageData: ImageData) {
|
||||
const image = new Image();
|
||||
image.onerror = () => handleImageLoad(true, imageData, image);
|
||||
image.onload = () => handleImageLoad(false, imageData, image);
|
||||
image.src = imageData.url;
|
||||
}
|
||||
|
||||
function handleImageLoad(err: boolean, imageData: ImageData, image: HTMLImageElement) {
|
||||
const {callbacks, url} = imageData;
|
||||
if (err) {
|
||||
images.del(url);
|
||||
} else {
|
||||
const {width, height} = image;
|
||||
imageData = {
|
||||
url,
|
||||
loaded: true,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
images.set(url, imageData);
|
||||
}
|
||||
if (callbacks) {
|
||||
callbacks.forEach(({callback}) => callback(err, imageData));
|
||||
}
|
||||
}
|
||||
|
||||
export function loadImage(url: string, callback: Function): ?Function {
|
||||
let imageData: {url: string, loaded: boolean, callbacks?: Set} = images.get(url);
|
||||
if (imageData && imageData.loaded) {
|
||||
callback(false, imageData);
|
||||
return null;
|
||||
} else {
|
||||
if (!imageData) {
|
||||
imageData = {
|
||||
url,
|
||||
loaded: false,
|
||||
};
|
||||
images.set(url, imageData);
|
||||
loadImageAsset(imageData);
|
||||
}
|
||||
if (!imageData.callbacks) {
|
||||
imageData.callbacks = new Set();
|
||||
}
|
||||
// Ensure each .loadImage call creates a unique `callback` in the .callbacks Set
|
||||
const container = {callback};
|
||||
imageData.callbacks.add(container);
|
||||
return () => {
|
||||
if (imageData.callbacks) {
|
||||
imageData.callbacks.delete(container);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ImageLoaderUtils.js
|
86
509bba0_unpacked/discord_app/utils/InstantInviteUtils.js
Executable file
86
509bba0_unpacked/discord_app/utils/InstantInviteUtils.js
Executable file
|
@ -0,0 +1,86 @@
|
|||
import i18n from '../i18n';
|
||||
import RegexUtils from './RegexUtils';
|
||||
|
||||
function makeOption(value, makeLabel) {
|
||||
return {
|
||||
value: `${value}`,
|
||||
|
||||
get label() {
|
||||
return makeLabel();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_AGE_OPTIONS = [
|
||||
makeOption(0, () => i18n.Messages.MAX_AGE_NEVER),
|
||||
makeOption(60 * 30, () => i18n.Messages.DURATION_MINUTES.format({minutes: 30})),
|
||||
makeOption(60 * 60, () => i18n.Messages.DURATION_HOURS.format({hours: 1})),
|
||||
makeOption(60 * 60 * 6, () => i18n.Messages.DURATION_HOURS.format({hours: 6})),
|
||||
makeOption(60 * 60 * 12, () => i18n.Messages.DURATION_HOURS.format({hours: 12})),
|
||||
makeOption(60 * 60 * 24, () => i18n.Messages.DURATION_DAYS.format({days: 1})),
|
||||
];
|
||||
|
||||
const MAX_USES_OPTIONS = [
|
||||
makeOption(0, () => i18n.Messages.MAX_USES.format({maxUses: 0})),
|
||||
makeOption(1, () => i18n.Messages.MAX_USES.format({maxUses: 1})),
|
||||
makeOption(5, () => i18n.Messages.MAX_USES.format({maxUses: 5})),
|
||||
makeOption(10, () => i18n.Messages.MAX_USES.format({maxUses: 10})),
|
||||
makeOption(25, () => i18n.Messages.MAX_USES.format({maxUses: 25})),
|
||||
makeOption(50, () => i18n.Messages.MAX_USES.format({maxUses: 50})),
|
||||
makeOption(100, () => i18n.Messages.MAX_USES.format({maxUses: 100})),
|
||||
];
|
||||
|
||||
const INVITE_RE = new RegExp(
|
||||
`${RegexUtils.escape(process.env.INVITE_HOST || location.host)}(?:/#)?(?:/invite)?/([a-z0-9\-]+)`,
|
||||
'ig'
|
||||
);
|
||||
|
||||
/**
|
||||
* Find all instant invite codes within the content of a message.
|
||||
*/
|
||||
export function findInvites(content: string): Array<string> {
|
||||
if (content == null) {
|
||||
return [];
|
||||
}
|
||||
const links = content.match(INVITE_RE);
|
||||
if (links != null) {
|
||||
return links.map(link => {
|
||||
const parts = link.split('/');
|
||||
return parts[parts.length - 1];
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single invite if it exists.
|
||||
*/
|
||||
export function findInvite(content: string): ?string {
|
||||
return findInvites(content)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an invite URL for a code.
|
||||
*/
|
||||
export function getInviteURL(code: string = ''): string {
|
||||
let host = process.env.INVITE_HOST;
|
||||
let path;
|
||||
if (host != null) {
|
||||
path = `/${code}`;
|
||||
} else {
|
||||
host = location.host;
|
||||
path = `/invite/${code}`;
|
||||
}
|
||||
return `${location.protocol}//${host}${path}`;
|
||||
}
|
||||
|
||||
export default {
|
||||
getMaxAgeOptions: MAX_AGE_OPTIONS,
|
||||
getMaxUsesOptions: MAX_USES_OPTIONS,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/InstantInviteUtils.js
|
16
509bba0_unpacked/discord_app/utils/InterceptionUtils.js
Executable file
16
509bba0_unpacked/discord_app/utils/InterceptionUtils.js
Executable file
|
@ -0,0 +1,16 @@
|
|||
import lodash from 'lodash';
|
||||
export const resolveThunk = thunk => (typeof thunk === 'function' ? thunk() : thunk);
|
||||
|
||||
// This function takes a promise, and is able to intercept it.
|
||||
export default lodash.curry((interceptor, isEnabled, originalPromiseFn) => {
|
||||
if (!resolveThunk(isEnabled)) {
|
||||
return originalPromiseFn({});
|
||||
} else {
|
||||
return interceptor(originalPromiseFn);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/InterceptionUtils.js
|
90
509bba0_unpacked/discord_app/utils/MFAInterceptionUtils.js
Executable file
90
509bba0_unpacked/discord_app/utils/MFAInterceptionUtils.js
Executable file
|
@ -0,0 +1,90 @@
|
|||
import lodash from 'lodash';
|
||||
import interceptRequest, {resolveThunk} from './InterceptionUtils';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import ModalActionCreators from '../actions/ModalActionCreators';
|
||||
|
||||
let showModal;
|
||||
if (__IOS__) {
|
||||
showModal = require('./ios/MFAInterceptionUtils').showModal;
|
||||
} else if (!__SDK__) {
|
||||
showModal = require('./web/MFAInterceptionUtils').showModal;
|
||||
}
|
||||
|
||||
const MFA_INVALID_CODE = 60008;
|
||||
|
||||
function mfaEnabled() {
|
||||
return UserStore.getCurrentUser().mfaEnabled;
|
||||
}
|
||||
|
||||
function needsMfaCode(res) {
|
||||
return res.body && res.body.code === MFA_INVALID_CODE;
|
||||
}
|
||||
|
||||
function requestMfaCode({promiseFn, resolve, reject, confirmModalProps, hooks: {onEarlyClose}}) {
|
||||
if (showModal == null) {
|
||||
onEarlyClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const key = showModal(handleSubmitCode, handleEarlyClose, confirmModalProps);
|
||||
|
||||
function handleEarlyClose() {
|
||||
onEarlyClose && onEarlyClose();
|
||||
}
|
||||
|
||||
function closeAndResolve(res) {
|
||||
ModalActionCreators.popWithKey(key);
|
||||
resolve(res);
|
||||
}
|
||||
|
||||
function closeAndReject(res) {
|
||||
ModalActionCreators.popWithKey(key);
|
||||
reject(res);
|
||||
}
|
||||
|
||||
function handleSubmitCode(code) {
|
||||
ModalActionCreators.update(key, {isLoading: true});
|
||||
executePromise({promiseFn, resolve: closeAndResolve, reject: closeAndReject, code, mfaCodeHandler: errorHandler});
|
||||
}
|
||||
|
||||
function errorHandler({res}) {
|
||||
ModalActionCreators.update(key, {
|
||||
error: res.body.message,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function executePromise({promiseFn, resolve, reject, code, mfaCodeHandler = requestMfaCode, ...extraOptions}) {
|
||||
promiseFn(code ? {code} : {}).then(resolve, res => {
|
||||
if (needsMfaCode(res)) {
|
||||
mfaCodeHandler({promiseFn, resolve, reject, res, ...extraOptions});
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mfaInterceptor = ({hooks = {}, ...extraOptions}, checkEnabled) =>
|
||||
interceptRequest(
|
||||
promiseFn =>
|
||||
new Promise((resolve, reject) => {
|
||||
(resolveThunk(checkEnabled) ? requestMfaCode : executePromise)({
|
||||
promiseFn,
|
||||
resolve,
|
||||
reject,
|
||||
hooks,
|
||||
...extraOptions,
|
||||
});
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
export default lodash.curry((confirmModalProps, promiseFn, {checkEnabled = mfaEnabled, ...extraOptions} = {}) => {
|
||||
return mfaInterceptor({confirmModalProps, ...extraOptions}, checkEnabled)(promiseFn);
|
||||
}, 2);
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/MFAInterceptionUtils.js
|
38
509bba0_unpacked/discord_app/utils/MFAUtils.js
Executable file
38
509bba0_unpacked/discord_app/utils/MFAUtils.js
Executable file
|
@ -0,0 +1,38 @@
|
|||
// This is based on node-authenticator but modified for browser use
|
||||
import b32 from 'thirty-two';
|
||||
|
||||
const crypto = (window && window.crypto) || window.msCrypto;
|
||||
export const hasCrypto = crypto && 'getRandomValues' in crypto && 'Uint8Array' in window;
|
||||
|
||||
// 10 cryptographically random binary bytes (80-bit key)
|
||||
function getRandomBytes(size = 10) {
|
||||
return crypto.getRandomValues(new Uint8Array(size));
|
||||
}
|
||||
|
||||
// Text-encode the key as base32 (in the style of Google Authenticator - same as Facebook, Microsoft, etc)
|
||||
function encodeTotpKey(bin) {
|
||||
// 32 ascii characters without trailing '='s
|
||||
const base32 = b32.encode(bin).toString('utf8').replace(/=/g, '');
|
||||
// lowercase with a space every 4 characters
|
||||
return base32.toLowerCase().replace(/(\w{4})/g, '$1 ').trim();
|
||||
}
|
||||
|
||||
export function generateTotpSecret() {
|
||||
return encodeTotpKey(getRandomBytes());
|
||||
}
|
||||
|
||||
export function encodeTotpSecret(secret) {
|
||||
return secret.replace(/[\s\.\_\-]+/g, '').toUpperCase();
|
||||
}
|
||||
|
||||
export function encodeTotpSecretAsUrl(accountName, secret, issuer = 'Discord') {
|
||||
// Full OTPAUTH URI spec as explained at https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
return `otpauth://totp/${encodeURI(issuer)}:${encodeURI(accountName)}\
|
||||
?secret=${encodeTotpSecret(secret)}\
|
||||
&issuer=${encodeURIComponent(issuer)}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/MFAUtils.js
|
83
509bba0_unpacked/discord_app/utils/MarkupASTUtils.js
Executable file
83
509bba0_unpacked/discord_app/utils/MarkupASTUtils.js
Executable file
|
@ -0,0 +1,83 @@
|
|||
function collectAst(ast, output = []) {
|
||||
if (Array.isArray(ast)) {
|
||||
ast.forEach(node => collectAst(node, output));
|
||||
} else if (typeof ast.content === 'string') {
|
||||
output.push(ast.content);
|
||||
} else if (ast.content != null) {
|
||||
collectAst(ast.content, output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function flattens a given markdown AST, removing redundant nodes. For example, the markdown:
|
||||
* `____________` would convert into an AST that transforms into `<em><em><em><em>__</em></em></em></em>`,
|
||||
* this function will flatten that into `<em>__</em>` removing the redundant em's.
|
||||
*
|
||||
* Chrome/electron handles these cases pretty badly, and on windows can crash if rendering or mousing over
|
||||
* a bunch of nested EMs. This also reduces the number of DOM elements that can be potentially rendered
|
||||
* in a bunch of other cases.
|
||||
*/
|
||||
function flattenAst(ast, parentAst = null) {
|
||||
// Walk the AST.
|
||||
if (Array.isArray(ast)) {
|
||||
const astLength = ast.length;
|
||||
for (let i = 0; i < astLength; i++) {
|
||||
ast[i] = flattenAst(ast[i], parentAst);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
// And more walking...
|
||||
if (ast.content != null) {
|
||||
ast.content = flattenAst(ast.content, ast);
|
||||
}
|
||||
|
||||
// Flatten the AST if the parent is the same as the current node type, we can just consume the content.
|
||||
if (parentAst != null && ast.type === parentAst.type) {
|
||||
return ast.content;
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function constrains the rendered AST to a given limit, discarding nodes once the limit is reached. This
|
||||
* prevents issues when a maliciously crafted message attempts to mount way too many nodes, causing the
|
||||
* client to become unresponsive.
|
||||
*/
|
||||
|
||||
const limitReached = new Function(); // TODO: replace with Symbol when we support it
|
||||
function constrainAst(ast, state = {limit: 200}) {
|
||||
if (ast.type !== 'text') {
|
||||
state.limit -= 1;
|
||||
if (state.limit <= 0) {
|
||||
return limitReached;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(ast)) {
|
||||
const astLength = ast.length;
|
||||
for (let i = 0; i < astLength; i++) {
|
||||
const newNode = constrainAst(ast[i], state);
|
||||
if (newNode === limitReached) {
|
||||
ast.length = i;
|
||||
break;
|
||||
}
|
||||
ast[i] = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
function astToString(ast) {
|
||||
return collectAst(ast).join('');
|
||||
}
|
||||
|
||||
export {astToString, flattenAst, constrainAst};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/MarkupASTUtils.js
|
294
509bba0_unpacked/discord_app/utils/MarkupUtils.js
Executable file
294
509bba0_unpacked/discord_app/utils/MarkupUtils.js
Executable file
|
@ -0,0 +1,294 @@
|
|||
/* @flow */
|
||||
|
||||
import {Permissions, ChannelTypes} from '../Constants';
|
||||
import SimpleMarkdown from 'simple-markdown';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import ChannelStore from '../stores/ChannelStore';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import NicknameUtils from './NicknameUtils';
|
||||
import PermissionUtils from './PermissionUtils';
|
||||
import UnicodeEmojis from '../lib/UnicodeEmojis';
|
||||
import lodash from 'lodash';
|
||||
import url from 'url';
|
||||
import punycode from 'punycode';
|
||||
|
||||
const DELETED_CHANNEL = 'deleted-channel';
|
||||
const DELETED_ROLE = 'deleted-role';
|
||||
|
||||
let MarkupUtils;
|
||||
if (__WEB__) {
|
||||
MarkupUtils = require('./web/MarkupUtils');
|
||||
} else {
|
||||
MarkupUtils = require('./ios/MarkupUtils');
|
||||
}
|
||||
|
||||
function depunycodeLink(target: string): string {
|
||||
try {
|
||||
const urlObject = url.parse(target);
|
||||
urlObject.hostname = punycode.toASCII(urlObject.hostname || '');
|
||||
return url.format(urlObject);
|
||||
} catch (e) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
function parseLink(capture) {
|
||||
const target = depunycodeLink(capture[1]);
|
||||
|
||||
return {
|
||||
type: 'link',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
content: target,
|
||||
},
|
||||
],
|
||||
target,
|
||||
title: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_RULES = {
|
||||
newline: SimpleMarkdown.defaultRules.newline,
|
||||
paragraph: SimpleMarkdown.defaultRules.paragraph,
|
||||
escape: SimpleMarkdown.defaultRules.escape,
|
||||
link: {
|
||||
...SimpleMarkdown.defaultRules.link,
|
||||
parse: (capture, parse, state) => {
|
||||
return {
|
||||
content: parse(capture[1], state),
|
||||
target: depunycodeLink(SimpleMarkdown.unescapeUrl(capture[2])),
|
||||
title: capture[3],
|
||||
};
|
||||
},
|
||||
},
|
||||
autolink: {
|
||||
...SimpleMarkdown.defaultRules.autolink,
|
||||
parse: parseLink,
|
||||
},
|
||||
url: {
|
||||
...SimpleMarkdown.defaultRules.url,
|
||||
parse: parseLink,
|
||||
},
|
||||
strong: SimpleMarkdown.defaultRules.strong,
|
||||
em: SimpleMarkdown.defaultRules.em,
|
||||
u: SimpleMarkdown.defaultRules.u,
|
||||
br: SimpleMarkdown.defaultRules.br,
|
||||
text: SimpleMarkdown.defaultRules.text,
|
||||
|
||||
inlineCode: SimpleMarkdown.defaultRules.inlineCode,
|
||||
|
||||
emoticon: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
// Match emoticons that have slashes in them that would otherwise be escaped.
|
||||
return /^(¯\\_\(ツ\)_\/¯)/.exec(source);
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
return {
|
||||
type: 'text',
|
||||
content: capture[1],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
codeBlock: {
|
||||
order: SimpleMarkdown.defaultRules.codeBlock.order,
|
||||
|
||||
match(source) {
|
||||
return /^```(([A-z0-9\-]+?)\n+)?\n*([^]+?)\n*```/.exec(source);
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
return {
|
||||
lang: (capture[2] || '').trim(),
|
||||
content: capture[3] || '',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
roleMention: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return /^<@&(\d+)>/.exec(source);
|
||||
},
|
||||
|
||||
parse([original, roleId], parse, state) {
|
||||
const selectedChannel = ChannelStore.getChannel(state.channelId);
|
||||
const guildId = selectedChannel ? selectedChannel.getGuildId() : null;
|
||||
const guild = guildId ? GuildStore.getGuild(guildId) : null;
|
||||
const role = guild ? guild.roles[roleId] : null;
|
||||
|
||||
if (role == null) {
|
||||
return {
|
||||
type: 'text',
|
||||
content: `@${DELETED_ROLE}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'mention',
|
||||
color: role.color,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
content: `@${role.name}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
mention: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return /^<@!?(\d+)>|^(@(?:everyone|here))/.exec(source);
|
||||
},
|
||||
|
||||
parse(capture, parse, state) {
|
||||
let name;
|
||||
let userId;
|
||||
const user = UserStore.getUser(capture[1]);
|
||||
if (user != null) {
|
||||
userId = user.id;
|
||||
name = user.toString();
|
||||
const channel = ChannelStore.getChannel(state.channelId);
|
||||
if (channel != null) {
|
||||
name = NicknameUtils.getNickname(channel.getGuildId(), state.channelId, user) || name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userId,
|
||||
channelId: state.channelId,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
content: name != null ? `@${name}` : capture[0],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
channel: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return /^<#(\d+)>/.exec(source);
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
const channel = ChannelStore.getChannel(capture[1]);
|
||||
|
||||
const user = UserStore.getCurrentUser();
|
||||
if (
|
||||
!channel ||
|
||||
channel.type !== ChannelTypes.GUILD_TEXT ||
|
||||
!PermissionUtils.can(Permissions.READ_MESSAGES, user, channel)
|
||||
) {
|
||||
return {
|
||||
type: 'text',
|
||||
content: channel != null ? `#${channel.toString()}` : `#${DELETED_CHANNEL}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
channelId: channel != null ? channel.id : null,
|
||||
guildId: channel != null ? channel.guild_id : null,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
content: channel != null ? `#${channel.toString()}` : `#${DELETED_CHANNEL}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
emoji: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return UnicodeEmojis.EMOJI_NAME_RE.exec(source);
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
const surrogate = UnicodeEmojis.convertNameToSurrogate(capture[1]);
|
||||
return {
|
||||
type: 'text',
|
||||
content: !surrogate ? `:${capture[1]}:` : surrogate,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
customEmoji: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return /^<:(\w+):(\d+)>/.exec(source);
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
return {
|
||||
type: 'text',
|
||||
content: `:${capture[1]}:`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
s: {
|
||||
order: SimpleMarkdown.defaultRules.u.order,
|
||||
match: SimpleMarkdown.inlineRegex(/^~~([\s\S]+?)~~(?!_)/),
|
||||
parse: SimpleMarkdown.defaultRules.u.parse,
|
||||
},
|
||||
};
|
||||
|
||||
const RULES = MarkupUtils.createRules({
|
||||
...DEFAULT_RULES,
|
||||
|
||||
link: {
|
||||
// this is used by other markdown rules, but we don't want to allow markdown 'link' matching
|
||||
...DEFAULT_RULES.link,
|
||||
match() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const CHANNEL_TOPIC_RULES = {
|
||||
...lodash.omit(RULES, ['inlineCode', 'codeBlock', 'br']),
|
||||
|
||||
codeBlock: {
|
||||
...RULES.codeBlock,
|
||||
|
||||
react: RULES.text.react,
|
||||
},
|
||||
};
|
||||
|
||||
const ALLOW_LINKS_RULES = MarkupUtils.createRules(DEFAULT_RULES);
|
||||
|
||||
const EMBED_TITLE_RULES = lodash.omit(RULES, ['codeBlock', 'br', 'mention', 'channel', 'roleMention']);
|
||||
|
||||
export default {
|
||||
...MarkupUtils,
|
||||
|
||||
getDefaultRules() {
|
||||
return {...RULES};
|
||||
},
|
||||
|
||||
parse: MarkupUtils.parserFor(RULES),
|
||||
parseAllowLinks: MarkupUtils.parserFor(ALLOW_LINKS_RULES),
|
||||
parseTopic: MarkupUtils.parserFor(CHANNEL_TOPIC_RULES),
|
||||
parseEmbedTitle: MarkupUtils.parserFor(EMBED_TITLE_RULES),
|
||||
parseReturnTree: MarkupUtils.parserFor(RULES, true),
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/MarkupUtils.js
|
639
509bba0_unpacked/discord_app/utils/MessageUtils.js
Executable file
639
509bba0_unpacked/discord_app/utils/MessageUtils.js
Executable file
|
@ -0,0 +1,639 @@
|
|||
/* @flow */
|
||||
|
||||
import Long from 'long';
|
||||
import SimpleMarkdown from 'simple-markdown';
|
||||
import type {Rule, Rules} from 'simple-markdown';
|
||||
import lodash from 'lodash';
|
||||
import type {LoDashArray} from 'lodash';
|
||||
import MarkupUtils from './MarkupUtils';
|
||||
import {toReactionEmoji} from './ReactionUtils';
|
||||
import AppAnalyticsUtils from './AppAnalyticsUtils';
|
||||
import UnicodeEmojis from '../lib/UnicodeEmojis';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import UserSettingsStore from '../stores/UserSettingsStore';
|
||||
import UserGuildSettingsStore from '../stores/UserGuildSettingsStore';
|
||||
import ChannelStore from '../stores/ChannelStore';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import GuildMemberStore from '../stores/GuildMemberStore';
|
||||
import EmojiStore from '../stores/EmojiStore';
|
||||
import MessageStore from '../stores/MessageStore';
|
||||
import SelectedChannelStore from '../stores/SelectedChannelStore';
|
||||
import ChangeNicknameActionCreators from '../actions/ChangeNicknameActionCreators';
|
||||
import MessageActionCreators from '../actions/MessageActionCreators';
|
||||
import EmojiUtils from './EmojiUtils';
|
||||
import {addReaction} from '../actions/ReactionActionCreators';
|
||||
import {ChannelTypes, ME, MessageStates, MessageTypes, LOCAL_BOT_ID, NON_USER_BOT_DISCRIMINATOR} from '../Constants';
|
||||
import type UserRecord from '../records/UserRecord';
|
||||
import type GuildRecord from '../records/GuildRecord';
|
||||
import type ChannelRecord from '../records/ChannelRecord';
|
||||
import type EmojiRecord from '../records/EmojiRecord';
|
||||
import type {Message} from '../flow/Server';
|
||||
|
||||
type ParserArray = LoDashArray<{id: string, text: string, hasNick?: boolean}>;
|
||||
|
||||
type ParserState = {
|
||||
inline: boolean,
|
||||
users: ParserArray,
|
||||
channels: ParserArray,
|
||||
mentionableRoles: ParserArray,
|
||||
customEmoticonsRegex: ?RegExp,
|
||||
customEmoji: {[key: string]: {id: string, name: string, originalName?: ?string}},
|
||||
textExclusions: string,
|
||||
guild: ?GuildRecord,
|
||||
emojiContext: Object,
|
||||
};
|
||||
|
||||
const COMMANDS = {
|
||||
tts: {
|
||||
action(parsed) {
|
||||
parsed.tts = UserSettingsStore.enableTTSCommand;
|
||||
},
|
||||
},
|
||||
me: {
|
||||
action(parsed) {
|
||||
parsed.content = `_${parsed.content}_`;
|
||||
},
|
||||
},
|
||||
tableflip: {
|
||||
action(parsed) {
|
||||
parsed.content = `${parsed.content} (╯°□°)╯︵ ┻━┻`.trim();
|
||||
},
|
||||
},
|
||||
unflip: {
|
||||
action(parsed) {
|
||||
parsed.content = `${parsed.content} ┬─┬ ノ( ゜-゜ノ)`.trim();
|
||||
},
|
||||
},
|
||||
shrug: {
|
||||
action(parsed) {
|
||||
parsed.content = `${parsed.content} ¯\\\_(ツ)\_/¯`.trim();
|
||||
},
|
||||
},
|
||||
nick: {
|
||||
action(parsed, {guildId, channelId}) {
|
||||
ChangeNicknameActionCreators.changeNickname(guildId, channelId, ME, parsed.content);
|
||||
parsed.content = '';
|
||||
},
|
||||
},
|
||||
reaction: {
|
||||
regex: /^\+:(.+?):$/,
|
||||
|
||||
action(parsed, {isEdit, channelId, emojiContext}) {
|
||||
if (isEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MessageStore.hasPresent(channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = MessageStore.getMessages(channelId).last();
|
||||
if (!message || !message.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emoji = emojiContext.getByName(parsed.content.slice(2, -1));
|
||||
if (emoji == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsed.content = '';
|
||||
addReaction(channelId, message.id, toReactionEmoji(emoji));
|
||||
},
|
||||
},
|
||||
searchReplace: {
|
||||
regex: /^s\/((?:.+?)[^\\]|.)\/(.*)/,
|
||||
|
||||
REMOVE_ESCAPE_CHARS: /\\([*?+/])/g,
|
||||
|
||||
action(parsed, context) {
|
||||
if (context.isEdit) {
|
||||
return;
|
||||
}
|
||||
const parsedContent = parsed.content;
|
||||
// We don't want to actually send the s/find/replace message, regardless
|
||||
// if it's valid or not
|
||||
parsed.content = '';
|
||||
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = MessageStore.getLastEditableMessage(channelId);
|
||||
if (!message || !message.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let [, error, correction] = Array.from(parsedContent.match(this.regex) || []);
|
||||
error = error.replace(this.REMOVE_ESCAPE_CHARS, (_, char) => char);
|
||||
correction = correction.replace(this.REMOVE_ESCAPE_CHARS, (_, char) => char);
|
||||
const content = message.content.replace(error, correction);
|
||||
if (!content && !message.attachments.length) {
|
||||
MessageActionCreators.deleteMessage(channelId, message.id);
|
||||
} else if (content !== message.content) {
|
||||
MessageActionCreators.editMessage(channelId, message.id, {content});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function handleCommands(parsed, context) {
|
||||
for (const key in COMMANDS) {
|
||||
const command = COMMANDS[key];
|
||||
if (command.regex) {
|
||||
if (command.regex.test(parsed.content)) {
|
||||
executeCommand(key, command, parsed, context);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (parsed.content[0] === '/') {
|
||||
const parts = parsed.content.split(' ');
|
||||
const parsedKey = parts[0].slice(1);
|
||||
if (key === parsedKey && command.action) {
|
||||
parsed.content = parts.slice(1).join(' ');
|
||||
executeCommand(key, command, parsed, context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function executeCommand(key, command, parsed, context) {
|
||||
parsed[key] = true;
|
||||
command.action(parsed, context);
|
||||
AppAnalyticsUtils.trackWithMetadata('slash_command_used', {
|
||||
command: key,
|
||||
});
|
||||
}
|
||||
|
||||
function matchPrefix(prefix: string, content: string, dataSource: ParserArray, type: ?string = null): any {
|
||||
if (content[0] !== prefix) return null;
|
||||
return dataSource
|
||||
.sortBy(({text}) => -text.length)
|
||||
.filter(({text}) => content.toLowerCase().indexOf(text.toLowerCase()) === 1)
|
||||
.map(({id, text, hasNick}) => [prefix + text, id, type, hasNick])
|
||||
.first();
|
||||
}
|
||||
|
||||
function matchAndReturnText(rule: Rule): Rule {
|
||||
return {
|
||||
order: rule.order,
|
||||
match: rule.match,
|
||||
parse(capture) {
|
||||
return {
|
||||
type: rule.type,
|
||||
content: capture[0],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const HERE_SENTINEL = '@here';
|
||||
const EVERYONE_SENTINEL = '@everyone';
|
||||
|
||||
function isMentioningSentinel(source: string): boolean {
|
||||
const firstSpaceIndex = source.indexOf(' ');
|
||||
const firstToken = firstSpaceIndex === -1 ? source : source.substr(0, firstSpaceIndex);
|
||||
return firstToken === EVERYONE_SENTINEL || firstToken === HERE_SENTINEL;
|
||||
}
|
||||
|
||||
const DEFAULT_RULES = MarkupUtils.getDefaultRules();
|
||||
const DEFAULT_TEXT_RULE = SimpleMarkdown.defaultRules.text;
|
||||
|
||||
// citron note: This lets us parse out items in our message in the correct order so we don't end up with
|
||||
// errors like smilies or @mentions in links. In addition to processing mentions & channel refs.
|
||||
const PARSE_RULES: Rules<ParserState> = {
|
||||
link: matchAndReturnText(SimpleMarkdown.defaultRules.link),
|
||||
autolink: matchAndReturnText(SimpleMarkdown.defaultRules.autolink),
|
||||
url: matchAndReturnText(SimpleMarkdown.defaultRules.url),
|
||||
inlineCode: matchAndReturnText(DEFAULT_RULES.inlineCode),
|
||||
codeBlock: matchAndReturnText(DEFAULT_RULES.codeBlock),
|
||||
mention: {
|
||||
match(source, state, prevCapture) {
|
||||
// Simple markdown splits on symbols, so we need prevCapture to prevent matching of emails
|
||||
// (if someone has the name "discord" and you type "support@discordapp.com" it would mention)
|
||||
const boundaryCapture = prevCapture.split(' ').pop() + source;
|
||||
if (/^[^ ]+@[^ ]+\.[^ \.]+/.test(boundaryCapture)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let match = matchPrefix('@', source, state.users, 'mention');
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
|
||||
match = matchPrefix('@', source, state.mentionableRoles, 'roleMention');
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
|
||||
if (isMentioningSentinel(source)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usersWithoutDiscriminator = state.users.map(user => {
|
||||
return {...user, text: user.text.split('#')[0]};
|
||||
});
|
||||
|
||||
return matchPrefix('@', source, usersWithoutDiscriminator, 'mention');
|
||||
},
|
||||
|
||||
parse([, id, type, hasNick]): mixed {
|
||||
let prefix = '@';
|
||||
if (type === 'roleMention') {
|
||||
prefix += '&';
|
||||
} else if (hasNick) {
|
||||
prefix += '!';
|
||||
}
|
||||
return {
|
||||
type,
|
||||
content: `<${prefix}${id}>`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
channel: {
|
||||
match(source, state) {
|
||||
return matchPrefix('#', source, state.channels);
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
return {
|
||||
type: 'text',
|
||||
content: `<#${capture[1]}>`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
emoticon: {
|
||||
match(source, state, prevSource) {
|
||||
if (!UserSettingsStore.convertEmoticons) return null;
|
||||
// Ensure the previous char is either the start of a string or a space.
|
||||
if (prevSource.length !== 0 && !/\s$/.test(prevSource)) return null;
|
||||
const match = UnicodeEmojis.EMOJI_SHORTCUT_RE.exec(source);
|
||||
if (match == null) return null;
|
||||
// Ensure the last char is either the end of a string or a space.
|
||||
if (match[0].length !== source.length && source[match[0].length] !== ' ') return null;
|
||||
return match;
|
||||
},
|
||||
|
||||
parse(capture) {
|
||||
return {
|
||||
type: 'emoticon',
|
||||
content: UnicodeEmojis.convertShortcutToName(capture[1]),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
emoji: {
|
||||
order: DEFAULT_RULES.emoji.order,
|
||||
match: DEFAULT_RULES.emoji.match,
|
||||
|
||||
parse([content, emojiName], recurseParse, {customEmoji}) {
|
||||
// if this is one of our custom emoticons but wrapped in colons, recognize it.
|
||||
const emoji = Object.prototype.hasOwnProperty.call(customEmoji, emojiName) ? customEmoji[emojiName] : null;
|
||||
|
||||
if (emoji != null) {
|
||||
return {
|
||||
type: 'customEmoticon',
|
||||
content: `<:${emoji.originalName || emoji.name}:${emoji.id}>`,
|
||||
emoji,
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise, just return as is to be handled normally
|
||||
return {
|
||||
type: 'text',
|
||||
content,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
customEmoticons: {
|
||||
match(source, state) {
|
||||
return state.customEmoticonsRegex && state.customEmoticonsRegex.exec(source);
|
||||
},
|
||||
|
||||
parse([content, emoticonName], recurseParse, {emojiContext}) {
|
||||
const emoji = emojiContext.getEmoticonByName(emoticonName);
|
||||
if (emoji != null) {
|
||||
return {
|
||||
type: 'customEmoticon',
|
||||
content: `<:${emoji.name}:${emoji.id}>`,
|
||||
emoji,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
content,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
text: {
|
||||
...DEFAULT_TEXT_RULE,
|
||||
|
||||
match(source, state) {
|
||||
if (state.textExclusions) {
|
||||
const re = `^[\\s\\S]+?(?=${state.textExclusions}|[^0-9A-Za-z\\s\\u00ff-\\uffff]|\\n\\n| {2,}\\n|\\w+:\\S|$)`;
|
||||
const reg = new RegExp(re);
|
||||
return reg.exec(source);
|
||||
} else if (DEFAULT_TEXT_RULE.match != null) {
|
||||
return DEFAULT_TEXT_RULE.match(source, state, '');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const UNPARSE_RULES: Rules<ParserState> = {
|
||||
inlineCode: matchAndReturnText(DEFAULT_RULES.inlineCode),
|
||||
codeBlock: matchAndReturnText(DEFAULT_RULES.codeBlock),
|
||||
|
||||
mention: {
|
||||
regex: /^<@!?(\d+)>/,
|
||||
|
||||
parse(capture): mixed {
|
||||
const user = UserStore.getUser(capture[1]);
|
||||
return {
|
||||
content: user == null ? capture[0] : `@${user.username}#${user.discriminator}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
roleMention: {
|
||||
regex: /^<@&(\d+)>/,
|
||||
|
||||
parse(capture, nestedParse, {guild}) {
|
||||
if (guild != null) {
|
||||
const role = guild.roles[capture[1]];
|
||||
if (role != null) {
|
||||
return {
|
||||
content: `@${role.name}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
content: capture[0],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
channel: {
|
||||
regex: /^<#(\d+)>/,
|
||||
|
||||
parse(capture) {
|
||||
const channel = ChannelStore.getChannel(capture[1]);
|
||||
return {
|
||||
content: channel == null ? capture[0] : channel.toString(true),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
emoji: {
|
||||
regex: /^<:(\w+):(\d+)>/,
|
||||
|
||||
parse([_, emojiName, emojiId], nestedParse, {guild}) {
|
||||
const existingEmoji = EmojiStore.getDisambiguatedEmojiContext(guild ? guild.id : null).getById(emojiId);
|
||||
const name = existingEmoji ? existingEmoji.name : emojiName;
|
||||
|
||||
return {
|
||||
content: `:${name}:`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
text: DEFAULT_TEXT_RULE,
|
||||
};
|
||||
|
||||
// sort rules in order of structure
|
||||
[PARSE_RULES, UNPARSE_RULES].forEach(rules => {
|
||||
Object.keys(rules).forEach((type, i) => {
|
||||
rules[type].order = i;
|
||||
});
|
||||
});
|
||||
|
||||
const parsePreprocessor = SimpleMarkdown.parserFor(PARSE_RULES);
|
||||
const unparsePreprocessor = SimpleMarkdown.parserFor(UNPARSE_RULES);
|
||||
|
||||
function rebuild(parsedItems, onText, onCustomEmoji) {
|
||||
let message = '';
|
||||
|
||||
parsedItems.forEach(item => {
|
||||
if (onCustomEmoji != null && item.type === 'customEmoticon') {
|
||||
onCustomEmoji(item.emoji);
|
||||
}
|
||||
|
||||
if (item.content.constructor === String) {
|
||||
if (item.type === 'codeBlock' || item.type === 'inlineCode') {
|
||||
message += item.content;
|
||||
} else {
|
||||
message += onText(item.content);
|
||||
}
|
||||
} else if (item.content.constructor === Array) {
|
||||
message += rebuild(item.content, onText, onCustomEmoji);
|
||||
} else {
|
||||
console.warn(`Unknown message item type: `, item);
|
||||
}
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function createNonce() {
|
||||
return Long.fromNumber(Date.now()).subtract(1420070400000).shiftLeft(22).toString();
|
||||
}
|
||||
|
||||
export type ParsedMessage = {
|
||||
content: string,
|
||||
tts: boolean,
|
||||
invalidEmojis?: Array<EmojiRecord>,
|
||||
};
|
||||
|
||||
export default {
|
||||
createMessage(channelId: string, content: string, tts: boolean = false): Message {
|
||||
const {id, username, avatar, discriminator, bot} = UserStore.getCurrentUser();
|
||||
return {
|
||||
id: createNonce(),
|
||||
type: MessageTypes.DEFAULT,
|
||||
content,
|
||||
channel_id: channelId, // eslint-disable-line camelcase
|
||||
author: {
|
||||
id,
|
||||
username,
|
||||
avatar,
|
||||
discriminator,
|
||||
bot,
|
||||
},
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentions: [],
|
||||
mention_roles: [], // eslint-disable-line camelcase
|
||||
mention_everyone: false, // eslint-disable-line camelcase
|
||||
timestamp: new Date().toISOString(),
|
||||
state: MessageStates.SENDING,
|
||||
tts,
|
||||
};
|
||||
},
|
||||
|
||||
createBotMessage(channelId: string, content: string): Message {
|
||||
return {
|
||||
id: createNonce(),
|
||||
type: MessageTypes.DEFAULT,
|
||||
content,
|
||||
channel_id: channelId, // eslint-disable-line camelcase
|
||||
author: {
|
||||
id: LOCAL_BOT_ID,
|
||||
username: 'Clyde',
|
||||
discriminator: NON_USER_BOT_DISCRIMINATOR,
|
||||
avatar: 'clyde',
|
||||
bot: true,
|
||||
},
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentions: [],
|
||||
mention_roles: [], // eslint-disable-line camelcase
|
||||
mention_everyone: false, // eslint-disable-line camelcase
|
||||
timestamp: new Date().toISOString(),
|
||||
state: MessageStates.SENT,
|
||||
tts: false,
|
||||
};
|
||||
},
|
||||
|
||||
parse(messageChannel: ChannelRecord, content: string, isEdit: ?boolean = false): ParsedMessage {
|
||||
const guildId = messageChannel.getGuildId();
|
||||
const guild = guildId ? GuildStore.getGuild(guildId) : null;
|
||||
|
||||
let members: Array<{userId: string, nick: ?string}>;
|
||||
if (messageChannel.isPrivate()) {
|
||||
members = messageChannel.recipients.map(userId => ({userId, nick: null}));
|
||||
} else if (guildId != null) {
|
||||
members = GuildMemberStore.getMembers(guildId).map(({userId, nick}) => ({userId, nick}));
|
||||
} else {
|
||||
members = [];
|
||||
}
|
||||
|
||||
const users = lodash(members).map(({userId, nick}) => {
|
||||
const user = UserStore.getUser(userId);
|
||||
return {
|
||||
id: userId,
|
||||
hasNick: nick != null,
|
||||
text: `${user.username}#${user.discriminator}`,
|
||||
};
|
||||
});
|
||||
|
||||
const mentionableRoles = lodash(guild != null ? guild.roles : {})
|
||||
.values()
|
||||
.filter(({mentionable}) => mentionable)
|
||||
.map(({id, name}) => ({id, text: name}));
|
||||
|
||||
const channels = lodash(ChannelStore.getChannels())
|
||||
.filter(channel => channel.guild_id === messageChannel.guild_id)
|
||||
.filter(channel => channel.type === ChannelTypes.GUILD_TEXT)
|
||||
.map(channel => {
|
||||
return {
|
||||
id: channel.id,
|
||||
text: channel.toString(),
|
||||
};
|
||||
});
|
||||
|
||||
const emojiContext = EmojiStore.getDisambiguatedEmojiContext(guildId);
|
||||
const textExclusions = emojiContext.getEscapedCustomEmoticonNames();
|
||||
const customEmoji = emojiContext.getCustomEmoji();
|
||||
const customEmoticonsRegex = emojiContext.getCustomEmoticonRegex();
|
||||
|
||||
const state = {
|
||||
inline: true,
|
||||
mentionableRoles,
|
||||
guild,
|
||||
users,
|
||||
channels,
|
||||
emojiContext,
|
||||
customEmoticonsRegex,
|
||||
customEmoji,
|
||||
textExclusions,
|
||||
};
|
||||
|
||||
const parsed = {
|
||||
content,
|
||||
tts: false,
|
||||
invalidEmojis: [],
|
||||
};
|
||||
|
||||
handleCommands(parsed, {
|
||||
guildId,
|
||||
guild,
|
||||
channelId: messageChannel.id,
|
||||
channel: messageChannel,
|
||||
isEdit,
|
||||
emojiContext,
|
||||
});
|
||||
|
||||
const usedEmoji = new Set();
|
||||
parsed.content = rebuild(
|
||||
parsePreprocessor(parsed.content, state),
|
||||
UnicodeEmojis.translateInlineEmojiToSurrogates,
|
||||
emoji => usedEmoji.add(emoji)
|
||||
);
|
||||
|
||||
usedEmoji.forEach(emoji => {
|
||||
if (EmojiUtils.isEmojiDisabled(emoji, messageChannel)) {
|
||||
parsed.invalidEmojis.push(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
return parsed;
|
||||
},
|
||||
|
||||
unparse(content: string, channelId: string): string {
|
||||
const selectedChannel = ChannelStore.getChannel(channelId);
|
||||
const guildId = selectedChannel ? selectedChannel.getGuildId() : null;
|
||||
const guild = guildId ? GuildStore.getGuild(guildId) : null;
|
||||
return rebuild(unparsePreprocessor(content, {inline: true, guild}), UnicodeEmojis.translateSurrogatesToInlineEmoji);
|
||||
},
|
||||
|
||||
isMentioned(user: UserRecord, message: {[key: string]: any}): boolean {
|
||||
const channel = ChannelStore.getChannel(message['channel_id']);
|
||||
if (channel == null) {
|
||||
console.warn(`MessageUtils.isMentioned(...): ${message['channel_id']} does not exist.`);
|
||||
return false;
|
||||
}
|
||||
const suppressEveryone = UserGuildSettingsStore.isSuppressEveryoneEnabled(channel.getGuildId());
|
||||
const mentionEveryone = (message.mentionEveryone || message['mention_everyone']) && !suppressEveryone;
|
||||
if (mentionEveryone) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
message.mentions.some(mention => (typeof mention === 'string' ? mention === user.id : mention.id === user.id))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const guildId = channel.getGuildId();
|
||||
if (guildId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const guild = GuildStore.getGuild(guildId);
|
||||
if (!guild) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const guildMember = GuildMemberStore.getMember(guild.id, user.id);
|
||||
if (!guildMember) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return message['mention_roles'].some(roleId => guildMember.roles.indexOf(roleId) !== -1);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/MessageUtils.js
|
107
509bba0_unpacked/discord_app/utils/NativeUtils.js
Executable file
107
509bba0_unpacked/discord_app/utils/NativeUtils.js
Executable file
|
@ -0,0 +1,107 @@
|
|||
let NativeUtils;
|
||||
if (__IOS__) {
|
||||
NativeUtils = require('./ios/NativeUtils');
|
||||
} else if (__SDK__) {
|
||||
NativeUtils = require('./sdk/NativeUtils');
|
||||
} else {
|
||||
NativeUtils = require('./web/NativeUtils');
|
||||
}
|
||||
|
||||
export default {
|
||||
...NativeUtils,
|
||||
|
||||
/**
|
||||
* Return true if the running version is equal to or newer than the requested version
|
||||
* Keys are the release channel. Value is an array. Element 0 is windows, Element 1 is OSX.
|
||||
*
|
||||
* @param {Object} requiredVersionsByChannel
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isVersionEqualOrNewer(requiredVersionsByChannel) {
|
||||
if (__IOS__) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const channelRequirements = requiredVersionsByChannel[this.releaseChannel];
|
||||
if (channelRequirements == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const platformRequirements = channelRequirements[this.platform];
|
||||
if (!platformRequirements) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const version = this.version;
|
||||
const needed = platformRequirements.split('.');
|
||||
for (let i = 0; i < version.length; ++i) {
|
||||
if (version[i] < parseInt(needed[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current client is Windows.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isWindows() {
|
||||
return /^win/.test(this.platform);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current client is OSX.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isOSX() {
|
||||
return this.platform === 'darwin';
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current client is Linux.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isLinux() {
|
||||
return this.platform === 'linux';
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current client is one of the desktop clients
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isDesktop() {
|
||||
if (__SDK__) {
|
||||
return false;
|
||||
}
|
||||
return this.isWindows() || this.isOSX() || this.isLinux();
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current client is iOS.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isIOS() {
|
||||
return __IOS__;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the current client is Android.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isAndroid() {
|
||||
return __ANDROID__;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/NativeUtils.js
|
15
509bba0_unpacked/discord_app/utils/NetworkUtils.js
Executable file
15
509bba0_unpacked/discord_app/utils/NetworkUtils.js
Executable file
|
@ -0,0 +1,15 @@
|
|||
/* @flow */
|
||||
|
||||
let NetworkUtils;
|
||||
if (__IOS__) {
|
||||
NetworkUtils = require('./ios/NetworkUtils');
|
||||
} else {
|
||||
NetworkUtils = require('./web/NetworkUtils');
|
||||
}
|
||||
|
||||
export default NetworkUtils;
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/NetworkUtils.js
|
31
509bba0_unpacked/discord_app/utils/NicknameUtils.js
Executable file
31
509bba0_unpacked/discord_app/utils/NicknameUtils.js
Executable file
|
@ -0,0 +1,31 @@
|
|||
import ChannelStore from '../stores/ChannelStore';
|
||||
import GuildMemberStore from '../stores/GuildMemberStore';
|
||||
|
||||
function getNickname(guildId, channelId, user) {
|
||||
if (guildId) {
|
||||
return GuildMemberStore.getNick(guildId, user.id);
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel && channel.isManaged()) {
|
||||
return channel.nicks[user.id];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getName(guildId, channelId, user) {
|
||||
return getNickname(guildId, channelId, user) || user.username;
|
||||
}
|
||||
|
||||
export default {
|
||||
getNickname: getNickname,
|
||||
getName: getName,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/NicknameUtils.js
|
12
509bba0_unpacked/discord_app/utils/NotificationUtils.js
Executable file
12
509bba0_unpacked/discord_app/utils/NotificationUtils.js
Executable file
|
@ -0,0 +1,12 @@
|
|||
let NotificationUtils;
|
||||
if (__IOS__) {
|
||||
NotificationUtils = require('./ios/NotificationUtils');
|
||||
} else {
|
||||
NotificationUtils = require('./web/NotificationUtils');
|
||||
}
|
||||
export default NotificationUtils;
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/NotificationUtils.js
|
554
509bba0_unpacked/discord_app/utils/PermissionUtils.js
Executable file
554
509bba0_unpacked/discord_app/utils/PermissionUtils.js
Executable file
|
@ -0,0 +1,554 @@
|
|||
/* @flow */
|
||||
|
||||
import ChannelRecord from '../records/ChannelRecord';
|
||||
import type {PermissionOverwrites} from '../records/ChannelRecord';
|
||||
import type GuildRecord, {Roles, Role} from '../records/GuildRecord';
|
||||
import type UserRecord from '../records/UserRecord';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import AuthenticationStore from '../stores/AuthenticationStore';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import GuildMemberStore from '../stores/GuildMemberStore';
|
||||
import {Permissions, ElevatedPermissions, MFALevels} from '../Constants';
|
||||
import i18n from '../i18n';
|
||||
import lodash from 'lodash';
|
||||
|
||||
type Context = ?ChannelRecord | ?GuildRecord;
|
||||
|
||||
const NONE = 0;
|
||||
const ALL = Object.keys(Permissions).reduce((permissions, key) => permissions | Permissions[key], NONE);
|
||||
const DEFAULT = [
|
||||
Permissions.CREATE_INSTANT_INVITE,
|
||||
Permissions.CHANGE_NICKNAME,
|
||||
|
||||
Permissions.READ_MESSAGES,
|
||||
Permissions.SEND_MESSAGES,
|
||||
Permissions.SEND_TSS_MESSAGES,
|
||||
Permissions.EMBED_LINKS,
|
||||
Permissions.ATTACH_FILES,
|
||||
Permissions.READ_MESSAGE_HISTORY,
|
||||
Permissions.MENTION_EVERYONE,
|
||||
Permissions.USE_EXTERNAL_EMOJIS,
|
||||
Permissions.ADD_REACTIONS,
|
||||
|
||||
Permissions.CONNECT,
|
||||
Permissions.SPEAK,
|
||||
Permissions.USE_VAD,
|
||||
].reduce((permissions, permission) => permissions | permission, NONE);
|
||||
|
||||
/**
|
||||
* Calculate a user's resulting permissions based on Guild & User MFA Settings
|
||||
*/
|
||||
function calculateElevatedPermissions(
|
||||
permissions: number,
|
||||
guild: GuildRecord,
|
||||
userId: string,
|
||||
checkElevated: boolean = true
|
||||
): number {
|
||||
if (checkElevated && guild.mfaLevel === MFALevels.ELEVATED && userId === AuthenticationStore.getId()) {
|
||||
if (!UserStore.getCurrentUser().mfaEnabled) {
|
||||
permissions &= ~ElevatedPermissions;
|
||||
}
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the user's permission mask in context.
|
||||
*/
|
||||
function computePermissions(
|
||||
user: UserRecord | string,
|
||||
context: Context,
|
||||
overwrites?: ?PermissionOverwrites,
|
||||
roles?: ?Roles,
|
||||
checkElevated: boolean = true
|
||||
): number {
|
||||
const userId = typeof user === 'string' ? user : user.id;
|
||||
let guild;
|
||||
|
||||
if (context instanceof ChannelRecord) {
|
||||
overwrites = overwrites != null ? {...context.permissionOverwrites, ...overwrites} : context.permissionOverwrites;
|
||||
const guildId = context.getGuildId();
|
||||
guild = guildId != null ? GuildStore.getGuild(guildId) : null;
|
||||
} else {
|
||||
overwrites = overwrites || {};
|
||||
guild = context;
|
||||
}
|
||||
|
||||
// If the context is null then do not continue.
|
||||
if (guild == null) {
|
||||
return NONE;
|
||||
}
|
||||
|
||||
// If the user is the owner then all permissions always apply.
|
||||
if (guild.isOwner(userId)) {
|
||||
return calculateElevatedPermissions(ALL, guild, userId, checkElevated);
|
||||
}
|
||||
|
||||
roles = roles != null ? {...guild.roles, ...roles} : guild.roles;
|
||||
|
||||
const member = GuildMemberStore.getMember(guild.id, userId);
|
||||
|
||||
// Start with the base permissions.
|
||||
const roleEveryone = roles[guild.id] /* EVERYONE */;
|
||||
let permissions = roleEveryone != null ? roleEveryone.permissions : DEFAULT;
|
||||
|
||||
// Apply the permissions of all the roles the member is a part of.
|
||||
if (member != null) {
|
||||
for (let i = 0; i < member.roles.length; i++) {
|
||||
const role = roles[member.roles[i]];
|
||||
// HACKFIX:
|
||||
// This shouldn't ever happen, but sometimes it does if there was a torn write to the database.
|
||||
// At least this will prevent the client from crashing on startup.
|
||||
if (role !== undefined) {
|
||||
permissions |= role.permissions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection JSBitwiseOperatorUsage
|
||||
if ((permissions & Permissions.ADMINISTRATOR) === Permissions.ADMINISTRATOR) {
|
||||
permissions = ALL;
|
||||
} else {
|
||||
// Apply the deny and allow of the everyone overwrite.
|
||||
const overwriteEveryone = overwrites[guild.id] /* EVERYONE */;
|
||||
if (overwriteEveryone != null) {
|
||||
permissions ^= permissions & overwriteEveryone.deny;
|
||||
permissions |= overwriteEveryone.allow;
|
||||
}
|
||||
|
||||
if (member != null) {
|
||||
// Batch apply allow denies and allows on all roles.
|
||||
let allow = NONE;
|
||||
let deny = NONE;
|
||||
for (let i = 0; i < member.roles.length; i++) {
|
||||
const overwriteRole = overwrites[member.roles[i]];
|
||||
if (overwriteRole != null) {
|
||||
allow |= overwriteRole.allow;
|
||||
deny |= overwriteRole.deny;
|
||||
}
|
||||
}
|
||||
permissions ^= permissions & deny;
|
||||
permissions |= allow;
|
||||
|
||||
// Apply member specific overwrite if it exist.
|
||||
const overwriteMember = overwrites[userId];
|
||||
if (overwriteMember != null) {
|
||||
permissions ^= permissions & overwriteMember.deny;
|
||||
permissions |= overwriteMember.allow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return calculateElevatedPermissions(permissions, guild, userId, checkElevated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a role is higher than another.
|
||||
*/
|
||||
function isRoleHigher(guild: GuildRecord, userId: string, a: ?Role, b: ?Role): boolean {
|
||||
if (guild.isOwner(userId)) return true;
|
||||
if (a == null) return false;
|
||||
const roles = lodash(guild.roles).sortBy(role => role.position).map(role => role.id).value();
|
||||
return roles.indexOf(a.id) > (b != null ? roles.indexOf(b.id) : -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the highest role or highest role for user.
|
||||
*/
|
||||
function getHighestRole(guild: GuildRecord, userId: string): ?Role {
|
||||
const member = GuildMemberStore.getMember(guild.id, userId);
|
||||
if (member == null) return null;
|
||||
return lodash(guild.roles)
|
||||
.filter(role => member.roles.indexOf(role.id) !== -1)
|
||||
.sortBy(role => -role.position)
|
||||
.first();
|
||||
}
|
||||
|
||||
function makeEveryoneOverwrite(id: ?string) {
|
||||
return {
|
||||
id,
|
||||
type: 'role',
|
||||
allow: NONE,
|
||||
deny: NONE,
|
||||
};
|
||||
}
|
||||
|
||||
function generateGuildGeneralPermissionSpec() {
|
||||
return {
|
||||
title: i18n.Messages.GENERAL_PERMISSIONS,
|
||||
permissions: [
|
||||
{
|
||||
title: i18n.Messages.ADMINISTRATOR,
|
||||
description: i18n.Messages.ADMINISTRATOR_DESCRIPTION,
|
||||
flag: Permissions.ADMINISTRATOR,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.VIEW_AUDIT_LOG,
|
||||
description: i18n.Messages.VIEW_AUDIT_LOG_DESCRIPTION,
|
||||
flag: Permissions.VIEW_AUDIT_LOG,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_SERVER,
|
||||
description: i18n.Messages.MANAGE_SERVER_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_GUILD,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_ROLES,
|
||||
description: i18n.Messages.MANAGE_ROLES_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_ROLES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_CHANNELS,
|
||||
description: i18n.Messages.MANAGE_CHANNELS_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_CHANNELS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.KICK_MEMBERS,
|
||||
flag: Permissions.KICK_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.BAN_MEMBERS,
|
||||
flag: Permissions.BAN_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.CREATE_INSTANT_INVITE,
|
||||
flag: Permissions.CREATE_INSTANT_INVITE,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.CHANGE_NICKNAME,
|
||||
description: i18n.Messages.CHANGE_NICKNAME_DESCRIPTION,
|
||||
flag: Permissions.CHANGE_NICKNAME,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_NICKNAMES,
|
||||
description: i18n.Messages.MANAGE_NICKNAMES_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_NICKNAMES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_EMOJIS,
|
||||
flag: Permissions.MANAGE_EMOJIS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_WEBHOOKS,
|
||||
description: i18n.Messages.MANAGE_WEBHOOKS_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_WEBHOOKS,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generateGuildTextPermissionSpec() {
|
||||
return {
|
||||
title: i18n.Messages.TEXT_PERMISSIONS,
|
||||
permissions: [
|
||||
{
|
||||
title: i18n.Messages.READ_MESSAGES,
|
||||
flag: Permissions.READ_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.SEND_MESSAGES,
|
||||
flag: Permissions.SEND_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.SEND_TTS_MESSAGES,
|
||||
description: i18n.Messages.SEND_TTS_MESSAGES_DESCRIPTION,
|
||||
flag: Permissions.SEND_TSS_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_MESSAGES,
|
||||
description: i18n.Messages.MANAGE_MESSAGES_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.EMBED_LINKS,
|
||||
flag: Permissions.EMBED_LINKS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.ATTACH_FILES,
|
||||
flag: Permissions.ATTACH_FILES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.READ_MESSAGE_HISTORY,
|
||||
flag: Permissions.READ_MESSAGE_HISTORY,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MENTION_EVERYONE,
|
||||
description: i18n.Messages.MENTION_EVERYONE_DESCRIPTION,
|
||||
flag: Permissions.MENTION_EVERYONE,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.USE_EXTERNAL_EMOJIS,
|
||||
description: i18n.Messages.USE_EXTERNAL_EMOJIS_DESCRIPTION,
|
||||
flag: Permissions.USE_EXTERNAL_EMOJIS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.ADD_REACTIONS,
|
||||
description: i18n.Messages.ADD_REACTIONS_DESCRIPTION,
|
||||
flag: Permissions.ADD_REACTIONS,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generateGuildVoicePermissionSpec() {
|
||||
return {
|
||||
title: i18n.Messages.VOICE_PERMISSIONS,
|
||||
permissions: [
|
||||
{
|
||||
title: i18n.Messages.CONNECT,
|
||||
flag: Permissions.CONNECT,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.SPEAK,
|
||||
flag: Permissions.SPEAK,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MUTE_MEMBERS,
|
||||
flag: Permissions.MUTE_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.DEAFEN_MEMBERS,
|
||||
flag: Permissions.DEAFEN_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MOVE_MEMBERS,
|
||||
description: i18n.Messages.MOVE_MEMBERS_DESCRIPTION,
|
||||
flag: Permissions.MOVE_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.USE_VAD,
|
||||
description: i18n.Messages.USE_VAD_DESCRIPTION,
|
||||
flag: Permissions.USE_VAD,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generateChannelGeneralPermissionSpec() {
|
||||
return {
|
||||
title: i18n.Messages.GENERAL_PERMISSIONS,
|
||||
permissions: [
|
||||
{
|
||||
title: i18n.Messages.CREATE_INSTANT_INVITE,
|
||||
flag: Permissions.CREATE_INSTANT_INVITE,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_CHANNEL,
|
||||
description: i18n.Messages.MANAGE_CHANNEL_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_CHANNELS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_PERMISSIONS,
|
||||
description: i18n.Messages.MANAGE_PERMISSIONS_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_ROLES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_WEBHOOKS,
|
||||
description: i18n.Messages.MANAGE_WEBHOOKS_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_WEBHOOKS,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generateChannelTextPermissionSpec() {
|
||||
return {
|
||||
title: i18n.Messages.TEXT_PERMISSIONS,
|
||||
permissions: [
|
||||
{
|
||||
title: i18n.Messages.READ_MESSAGES,
|
||||
flag: Permissions.READ_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.SEND_MESSAGES,
|
||||
flag: Permissions.SEND_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.SEND_TTS_MESSAGES,
|
||||
description: i18n.Messages.SEND_TTS_MESSAGES_DESCRIPTION,
|
||||
flag: Permissions.SEND_TSS_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MANAGE_MESSAGES,
|
||||
description: i18n.Messages.MANAGE_MESSAGES_DESCRIPTION,
|
||||
flag: Permissions.MANAGE_MESSAGES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.EMBED_LINKS,
|
||||
flag: Permissions.EMBED_LINKS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.ATTACH_FILES,
|
||||
flag: Permissions.ATTACH_FILES,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.READ_MESSAGE_HISTORY,
|
||||
flag: Permissions.READ_MESSAGE_HISTORY,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MENTION_EVERYONE,
|
||||
description: i18n.Messages.MENTION_EVERYONE_DESCRIPTION,
|
||||
flag: Permissions.MENTION_EVERYONE,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.USE_EXTERNAL_EMOJIS,
|
||||
description: i18n.Messages.USE_EXTERNAL_EMOJIS_DESCRIPTION,
|
||||
flag: Permissions.USE_EXTERNAL_EMOJIS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.ADD_REACTIONS,
|
||||
description: i18n.Messages.ADD_REACTIONS_DESCRIPTION,
|
||||
flag: Permissions.ADD_REACTIONS,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function generateChannelVoicePermissionSpec() {
|
||||
return {
|
||||
title: i18n.Messages.VOICE_PERMISSIONS,
|
||||
permissions: [
|
||||
{
|
||||
title: i18n.Messages.CONNECT,
|
||||
flag: Permissions.CONNECT,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.SPEAK,
|
||||
flag: Permissions.SPEAK,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MUTE_MEMBERS,
|
||||
flag: Permissions.MUTE_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.DEAFEN_MEMBERS,
|
||||
flag: Permissions.DEAFEN_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.MOVE_MEMBERS,
|
||||
description: i18n.Messages.MOVE_MEMBERS_DESCRIPTION,
|
||||
flag: Permissions.MOVE_MEMBERS,
|
||||
},
|
||||
{
|
||||
title: i18n.Messages.USE_VAD,
|
||||
description: i18n.Messages.USE_VAD_DESCRIPTION,
|
||||
flag: Permissions.USE_VAD,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
type PermissionSpecItem = {
|
||||
title: string,
|
||||
description?: string,
|
||||
flag: number,
|
||||
};
|
||||
|
||||
type PermissionSpec = {
|
||||
title: string,
|
||||
permissions: Array<PermissionSpecItem>,
|
||||
};
|
||||
|
||||
function generatePermissionSpec(): Array<PermissionSpec> {
|
||||
return [generateGuildGeneralPermissionSpec(), generateGuildTextPermissionSpec(), generateGuildVoicePermissionSpec()];
|
||||
}
|
||||
|
||||
export default {
|
||||
PASSTHROUGH: 'PASSTHROUGH',
|
||||
ALLOW: 'ALLOW',
|
||||
DENY: 'DENY',
|
||||
|
||||
NONE,
|
||||
DEFAULT,
|
||||
ALL,
|
||||
|
||||
computePermissions,
|
||||
isRoleHigher,
|
||||
getHighestRole,
|
||||
|
||||
/**
|
||||
* Determine if user has this permission within context.
|
||||
*/
|
||||
can(
|
||||
permission: number,
|
||||
user: UserRecord | string,
|
||||
context: Context,
|
||||
overwrites: ?PermissionOverwrites,
|
||||
roles?: Roles
|
||||
): boolean {
|
||||
return (computePermissions(user, context, overwrites, roles) & permission) === permission;
|
||||
},
|
||||
|
||||
canEveryoneRole(permission: number, context: Context) {
|
||||
let guild;
|
||||
let overwrites = {};
|
||||
if (context instanceof ChannelRecord) {
|
||||
overwrites = context.permissionOverwrites;
|
||||
const guildId = context.getGuildId();
|
||||
guild = guildId != null ? GuildStore.getGuild(guildId) : null;
|
||||
} else {
|
||||
guild = context;
|
||||
}
|
||||
if (guild == null) {
|
||||
return false;
|
||||
}
|
||||
const overwrite = overwrites[guild.id];
|
||||
if (overwrite != null) {
|
||||
if ((overwrite.deny & permission) === permission) {
|
||||
return false;
|
||||
}
|
||||
if ((overwrite.allow & permission) === permission) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const everyoneRole = guild.roles[guild.id];
|
||||
if ((everyoneRole.permissions & permission) !== permission) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if everyone has this permission within context.
|
||||
*/
|
||||
canEveryone(permission: number, context: Context): boolean {
|
||||
let guild;
|
||||
let overwrites = {};
|
||||
if (context instanceof ChannelRecord) {
|
||||
overwrites = context.permissionOverwrites;
|
||||
const guildId = context.getGuildId();
|
||||
guild = guildId != null ? GuildStore.getGuild(guildId) : null;
|
||||
} else {
|
||||
guild = context;
|
||||
}
|
||||
if (guild == null) {
|
||||
return false;
|
||||
}
|
||||
const everyoneRole = guild.roles[guild.id];
|
||||
if ((everyoneRole.permissions & permission) !== permission) {
|
||||
return false;
|
||||
}
|
||||
// Every role in the guild must have this this permission.
|
||||
if (lodash.some(overwrites, overwrite => (overwrite.deny & permission) === permission)) {
|
||||
// No overwrite can deny this permission.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
makeEveryoneOverwrite,
|
||||
generateChannelGeneralPermissionSpec,
|
||||
generateChannelTextPermissionSpec,
|
||||
generateChannelVoicePermissionSpec,
|
||||
|
||||
generateGuildGeneralPermissionSpec,
|
||||
generateGuildTextPermissionSpec,
|
||||
generateGuildVoicePermissionSpec,
|
||||
|
||||
generatePermissionSpec,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/PermissionUtils.js
|
19
509bba0_unpacked/discord_app/utils/PrivateChannelRecipientsUtils.js
Executable file
19
509bba0_unpacked/discord_app/utils/PrivateChannelRecipientsUtils.js
Executable file
|
@ -0,0 +1,19 @@
|
|||
/* flow */
|
||||
|
||||
import lodash from 'lodash';
|
||||
import UserStore from '../stores/UserStore';
|
||||
|
||||
export function getRecipients(recipients: Array<string>) {
|
||||
return lodash(recipients)
|
||||
.map(UserStore.getUser)
|
||||
.filter(user => user != null)
|
||||
.unshift(UserStore.getCurrentUser())
|
||||
.sortBy(user => user.username.toLowerCase())
|
||||
.map(user => ({key: user.id, user}))
|
||||
.value();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/PrivateChannelRecipientsUtils.js
|
66
509bba0_unpacked/discord_app/utils/Queue.js
Executable file
66
509bba0_unpacked/discord_app/utils/Queue.js
Executable file
|
@ -0,0 +1,66 @@
|
|||
import Deque from 'double-ended-queue';
|
||||
import Logger from '../lib/Logger';
|
||||
|
||||
const defaultLogger = Logger.create('Queue');
|
||||
|
||||
export default class Queue {
|
||||
/**
|
||||
* This implements a queue that will call `drain(message, callback)` in the order that messages are enqueued.
|
||||
* The callback is expected to be called back with (err: {retryAfter}, result). The retryAfter property of the error
|
||||
* hints the queue as to how long it should wait before attempting to drain the queue again. Additionally,
|
||||
* when the callback is called with an error, it will enqueue the message to drained again from the front of
|
||||
* the queue.
|
||||
*/
|
||||
constructor({defaultRetryAfter = 100, logger = defaultLogger}) {
|
||||
this.queue = new Deque();
|
||||
this.timeout = null;
|
||||
this.draining = false;
|
||||
this.defaultRetryAfter = defaultRetryAfter;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
enqueue(message, completed, started = null, progress = null) {
|
||||
this.queue.push({message, completed, started, progress});
|
||||
this._drainIfNecessary();
|
||||
}
|
||||
|
||||
_drainIfNecessary() {
|
||||
if (this.timeout !== null) return;
|
||||
if (this.queue.length === 0) return;
|
||||
if (this.draining === true) return;
|
||||
this.draining = true;
|
||||
|
||||
const {message, ...callbacks} = this.queue.shift();
|
||||
const completed = (err, res) => {
|
||||
this.draining = false;
|
||||
if (!err) {
|
||||
setImmediate(() => this._drainIfNecessary());
|
||||
try {
|
||||
callbacks.completed(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else {
|
||||
const retryAfter = err.retryAfter || this.defaultRetryAfter;
|
||||
this.logger.info(`Rate limited. Delaying draining of queue for ${retryAfter} ms.`);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.queue.unshift({message, ...callbacks});
|
||||
this.timeout = null;
|
||||
this._drainIfNecessary();
|
||||
}, retryAfter);
|
||||
}
|
||||
};
|
||||
|
||||
this.drain(message, completed, callbacks.started, callbacks.progress);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
drain(message, completed, started, progress) {
|
||||
throw 'Not Implemented';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/Queue.js
|
34
509bba0_unpacked/discord_app/utils/QuickSwitcherUtils.js
Executable file
34
509bba0_unpacked/discord_app/utils/QuickSwitcherUtils.js
Executable file
|
@ -0,0 +1,34 @@
|
|||
import {QuickSwitcherResultTypes} from '../Constants';
|
||||
|
||||
export function findNextSelected(direction, index, results, originalValue) {
|
||||
const {length} = results;
|
||||
if (length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (originalValue == null) {
|
||||
originalValue = index;
|
||||
} else if (originalValue === index) {
|
||||
// We've looped back around to the original index and we should stop to
|
||||
// prevent an infinite loop
|
||||
return index;
|
||||
}
|
||||
|
||||
// Increment index and wrap if it overflows
|
||||
index += direction === 'up' ? -1 : 1;
|
||||
if (index < 0 || index >= length) {
|
||||
return findNextSelected(direction, index < 0 ? length : -1, results, originalValue);
|
||||
}
|
||||
|
||||
// Check if result is valid
|
||||
const nextResult = results[index];
|
||||
if (nextResult.type === QuickSwitcherResultTypes.HEADER) {
|
||||
return findNextSelected(direction, index, results, originalValue);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/QuickSwitcherUtils.js
|
71
509bba0_unpacked/discord_app/utils/RTCConnectionUtils.js
Executable file
71
509bba0_unpacked/discord_app/utils/RTCConnectionUtils.js
Executable file
|
@ -0,0 +1,71 @@
|
|||
/* @flow */
|
||||
|
||||
import {RTCConnectionStates} from '../Constants';
|
||||
import i18n from '../i18n';
|
||||
|
||||
function getStatus(rtcConnectionState: $Keys<typeof RTCConnectionStates>, hasVideo?: boolean = false) {
|
||||
let connectionStatus;
|
||||
let connectionStatusText;
|
||||
|
||||
switch (rtcConnectionState) {
|
||||
case RTCConnectionStates.CONNECTED:
|
||||
connectionStatus = 'connected';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_CONNECTED;
|
||||
break;
|
||||
case RTCConnectionStates.CONNECTING:
|
||||
connectionStatus = 'connecting';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_CONNECTING;
|
||||
break;
|
||||
case RTCConnectionStates.AUTHENTICATING:
|
||||
connectionStatus = 'connecting';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_AUTHENTICATING;
|
||||
break;
|
||||
case RTCConnectionStates.AWAITING_ENDPOINT:
|
||||
connectionStatus = 'connecting';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_AWAITING_ENDPOINT;
|
||||
break;
|
||||
case RTCConnectionStates.RTC_CONNECTED:
|
||||
connectionStatus = 'connected';
|
||||
if (hasVideo) {
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_VIDEO_CONNECTED;
|
||||
} else {
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_VOICE_CONNECTED;
|
||||
}
|
||||
break;
|
||||
case RTCConnectionStates.RTC_CONNECTING:
|
||||
connectionStatus = 'connecting';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_RTC_CONNECTING;
|
||||
break;
|
||||
case RTCConnectionStates.ICE_CHECKING:
|
||||
connectionStatus = 'connecting';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_ICE_CHECKING;
|
||||
break;
|
||||
case RTCConnectionStates.NO_ROUTE:
|
||||
connectionStatus = 'error';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_NO_ROUTE;
|
||||
break;
|
||||
case RTCConnectionStates.RTC_DISCONNECTED:
|
||||
connectionStatus = 'error';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_RTC_DISCONNECTED;
|
||||
break;
|
||||
case RTCConnectionStates.DISCONNECTED:
|
||||
default:
|
||||
connectionStatus = 'error';
|
||||
connectionStatusText = i18n.Messages.CONNECTION_STATUS_DISCONNECTED;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
connectionStatus,
|
||||
connectionStatusText,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
getStatus,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/RTCConnectionUtils.js
|
118
509bba0_unpacked/discord_app/utils/ReactionUtils.js
Executable file
118
509bba0_unpacked/discord_app/utils/ReactionUtils.js
Executable file
|
@ -0,0 +1,118 @@
|
|||
/* @flow */
|
||||
|
||||
import lodash from 'lodash';
|
||||
import i18n from '../i18n';
|
||||
import MessageReactionsStore from '../stores/MessageReactionsStore';
|
||||
import RelationshipStore from '../stores/RelationshipStore';
|
||||
import ChannelStore from '../stores/ChannelStore';
|
||||
import GuildMemberStore from '../stores/GuildMemberStore';
|
||||
import MessageRecord from '../records/MessageRecord';
|
||||
import UnicodeEmojis from '../lib/UnicodeEmojis';
|
||||
import type {Emoji} from '../lib/UnicodeEmojis';
|
||||
|
||||
export type ReactionEmoji = {
|
||||
id: ?string,
|
||||
name: string,
|
||||
};
|
||||
|
||||
export function getReactionTooltip(message: MessageRecord, emoji: ReactionEmoji): string {
|
||||
let users = MessageReactionsStore.getReactions(message.getChannelId(), message.id, emoji);
|
||||
|
||||
const channel = ChannelStore.getChannel(message.getChannelId());
|
||||
const guildId = channel != null && !channel.isPrivate() ? channel.getGuildId() : null;
|
||||
|
||||
users = lodash(users)
|
||||
// Don't name blocked users.
|
||||
.reject(user => RelationshipStore.isBlocked(user.id))
|
||||
.take(3)
|
||||
.map(user => (guildId != null && GuildMemberStore.getNick(guildId, user.id)) || user.username)
|
||||
.value();
|
||||
|
||||
if (!users.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const reaction = message.getReaction(emoji);
|
||||
const othersCount = Math.max(0, ((reaction && reaction.count) || 0) - users.length);
|
||||
|
||||
let emojiName;
|
||||
if (emoji.id == null) {
|
||||
emojiName = UnicodeEmojis.convertSurrogateToName(emoji.name);
|
||||
} else {
|
||||
emojiName = `:${emoji.name}:`;
|
||||
}
|
||||
|
||||
if (users.length === 1) {
|
||||
if (othersCount > 0) {
|
||||
return i18n.Messages.REACTION_TOOLTIP_1_N.format({
|
||||
a: users[0],
|
||||
n: othersCount,
|
||||
emojiName,
|
||||
});
|
||||
} else {
|
||||
return i18n.Messages.REACTION_TOOLTIP_1.format({
|
||||
a: users[0],
|
||||
emojiName,
|
||||
});
|
||||
}
|
||||
} else if (users.length === 2) {
|
||||
if (othersCount > 0) {
|
||||
return i18n.Messages.REACTION_TOOLTIP_2_N.format({
|
||||
a: users[0],
|
||||
b: users[1],
|
||||
n: othersCount,
|
||||
emojiName,
|
||||
});
|
||||
} else {
|
||||
return i18n.Messages.REACTION_TOOLTIP_2.format({
|
||||
a: users[0],
|
||||
b: users[1],
|
||||
emojiName,
|
||||
});
|
||||
}
|
||||
} else if (users.length === 3) {
|
||||
if (othersCount > 0) {
|
||||
return i18n.Messages.REACTION_TOOLTIP_3_N.format({
|
||||
a: users[0],
|
||||
b: users[1],
|
||||
c: users[2],
|
||||
n: othersCount,
|
||||
emojiName,
|
||||
});
|
||||
} else {
|
||||
return i18n.Messages.REACTION_TOOLTIP_3.format({
|
||||
a: users[0],
|
||||
b: users[1],
|
||||
c: users[2],
|
||||
emojiName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return i18n.Messages.REACTION_TOOLTIP_N.format({
|
||||
n: othersCount,
|
||||
emojiName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function toReactionEmoji(emoji: Emoji): ReactionEmoji {
|
||||
return {
|
||||
id: emoji.id || null,
|
||||
name: emoji.id ? emoji.name : emoji.surrogatePair,
|
||||
};
|
||||
}
|
||||
|
||||
export function emojiEquals(reactionEmoji: ReactionEmoji, emoji: Emoji | ReactionEmoji) {
|
||||
// If a custom emoji, only compare by id, as name can vary.
|
||||
if (emoji.id != null) {
|
||||
return emoji.id == reactionEmoji.id;
|
||||
}
|
||||
|
||||
// If not a custom emoji, compare by surrogatePair
|
||||
return reactionEmoji.id == null && emoji.name == reactionEmoji.name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ReactionUtils.js
|
29
509bba0_unpacked/discord_app/utils/RedirectUtils.js
Executable file
29
509bba0_unpacked/discord_app/utils/RedirectUtils.js
Executable file
|
@ -0,0 +1,29 @@
|
|||
/* @flow */
|
||||
|
||||
import PostConnectionCallbackActionCreators from '../actions/PostConnectionCallbackActionCreators';
|
||||
import UserSettingsModalActionCreators from '../actions/UserSettingsModalActionCreators';
|
||||
import {UserSettingsSections, Routes} from '../Constants';
|
||||
|
||||
export function isSafeRedirect(url: string): boolean {
|
||||
return /^\/[^\/\\]/.test(url);
|
||||
}
|
||||
|
||||
export function userSettingsRedirector(nextState: {params: {section: string, subsection: string}}, replace: Function) {
|
||||
let {section, subsection} = nextState.params;
|
||||
section = section && section.toUpperCase();
|
||||
subsection = subsection && subsection.toUpperCase();
|
||||
|
||||
if (
|
||||
UserSettingsSections.hasOwnProperty(section) &&
|
||||
(!subsection || UserSettingsSections.hasOwnProperty(subsection))
|
||||
) {
|
||||
PostConnectionCallbackActionCreators.call(() => UserSettingsModalActionCreators.open(section, subsection));
|
||||
}
|
||||
|
||||
replace({pathname: Routes.ME});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/RedirectUtils.js
|
15
509bba0_unpacked/discord_app/utils/RegexUtils.js
Executable file
15
509bba0_unpacked/discord_app/utils/RegexUtils.js
Executable file
|
@ -0,0 +1,15 @@
|
|||
/* @flow */
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Escape all valid Regex expressions in a string.
|
||||
*/
|
||||
escape(str: string): string {
|
||||
return str.replace(/[\-\[\]\/\{}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/RegexUtils.js
|
43
509bba0_unpacked/discord_app/utils/RelationshipUtils.js
Executable file
43
509bba0_unpacked/discord_app/utils/RelationshipUtils.js
Executable file
|
@ -0,0 +1,43 @@
|
|||
/* @flow */
|
||||
|
||||
import RouterUtils from './RouterUtils';
|
||||
import AvatarUtils from './AvatarUtils';
|
||||
import FriendsActionCreators from '../actions/FriendsActionCreators';
|
||||
import ChannelActionCreators from '../actions/ChannelActionCreators';
|
||||
import NotificationActionCreators from '../actions/NotificationActionCreators';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import i18n from '../i18n';
|
||||
import {Routes, FriendsSections} from '../Constants';
|
||||
|
||||
type User = {
|
||||
id: string,
|
||||
username: string,
|
||||
discriminator: string,
|
||||
avatar: ?string,
|
||||
bot?: boolean,
|
||||
};
|
||||
|
||||
function showNotification(user: User, body: string, onClick: Function) {
|
||||
NotificationActionCreators.showNotification(AvatarUtils.getUserAvatarURL(user), user.username, body, {
|
||||
tag: user.id,
|
||||
onClick,
|
||||
});
|
||||
}
|
||||
|
||||
export function showPendingNotification(user: User) {
|
||||
showNotification(user, i18n.Messages.NOTIFICATION_PENDING_FRIEND_REQUEST, () => {
|
||||
RouterUtils.transitionTo(Routes.FRIENDS);
|
||||
FriendsActionCreators.setSection(FriendsSections.PENDING);
|
||||
});
|
||||
}
|
||||
|
||||
export function showAcceptedNotification(user: User) {
|
||||
showNotification(user, i18n.Messages.NOTIFICATION_ACCEPTED_FRIEND_REQUEST, () => {
|
||||
ChannelActionCreators.openPrivateChannel(UserStore.getCurrentUser().id, user.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/RelationshipUtils.js
|
53
509bba0_unpacked/discord_app/utils/ReportUtils.js
Executable file
53
509bba0_unpacked/discord_app/utils/ReportUtils.js
Executable file
|
@ -0,0 +1,53 @@
|
|||
/* @flow */
|
||||
|
||||
import {ExperimentBuckets, ExperimentTypes} from '../Constants';
|
||||
import AuthenticationStore from '../stores/AuthenticationStore';
|
||||
import ReportExperimentStore from '../stores/experiments/ReportExperimentStore';
|
||||
import ExperimentActonCreators from '../actions/ExperimentActionCreators';
|
||||
import type UserRecord from '../records/UserRecord';
|
||||
import type MessageRecord from '../records/MessageRecord';
|
||||
import type ChannelRecord from '../records/ChannelRecord';
|
||||
|
||||
// Manually register experiment.
|
||||
ExperimentActonCreators.register(ReportExperimentStore, {
|
||||
[ExperimentTypes.USER]: {
|
||||
[ExperimentBuckets.CONTROL]: () => null,
|
||||
[ExperimentBuckets.TREATMENT_1]: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
function canReportInChannel(channel: ?ChannelRecord): boolean {
|
||||
return channel != null && ReportExperimentStore.canReport();
|
||||
}
|
||||
|
||||
function canReportUser(user: ?UserRecord): boolean {
|
||||
if (!ReportExperimentStore.canReport()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
const userId = user.id;
|
||||
|
||||
if (AuthenticationStore.getId() == userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function canReportMessage(message: ?MessageRecord) {
|
||||
return message != null && canReportUser(message.author);
|
||||
}
|
||||
|
||||
export default {
|
||||
canReportUser,
|
||||
canReportInChannel,
|
||||
canReportMessage,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ReportUtils.js
|
35
509bba0_unpacked/discord_app/utils/RouterUtils.js
Executable file
35
509bba0_unpacked/discord_app/utils/RouterUtils.js
Executable file
|
@ -0,0 +1,35 @@
|
|||
let RouterUtils = {
|
||||
/**
|
||||
* Transitions to the URL specified in the arguments by pushing
|
||||
* a new URL onto the history stack.
|
||||
*
|
||||
* @param {String} _path
|
||||
*/
|
||||
transitionTo(_path) {},
|
||||
|
||||
/**
|
||||
* Transitions to the URL specified in the arguments by replacing
|
||||
* the current URL in the history stack.
|
||||
*
|
||||
* @param {String} _path
|
||||
*/
|
||||
replaceWith(_path) {},
|
||||
|
||||
/**
|
||||
* Gets the history object
|
||||
*
|
||||
* @returns {Object} history
|
||||
*/
|
||||
getHistory() {},
|
||||
};
|
||||
|
||||
if (__WEB__ && !__SDK__) {
|
||||
RouterUtils = require('./web/RouterUtils');
|
||||
}
|
||||
|
||||
export default RouterUtils;
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/RouterUtils.js
|
71
509bba0_unpacked/discord_app/utils/ScriptLoaderUtils.js
Executable file
71
509bba0_unpacked/discord_app/utils/ScriptLoaderUtils.js
Executable file
|
@ -0,0 +1,71 @@
|
|||
const SCRIPTS = new Map();
|
||||
const REGISTRY = new Map();
|
||||
const SCRIPT_LOAD_TIMEOUT = 3000;
|
||||
|
||||
function removeScript(script) {
|
||||
if (script && script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
}
|
||||
|
||||
function loadNewScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let failed = false;
|
||||
const globalKey = REGISTRY.get(url);
|
||||
const onScriptError = () => {
|
||||
if (failed) {
|
||||
return;
|
||||
}
|
||||
failed = true;
|
||||
removeScript(script);
|
||||
clearTimeout(timeout);
|
||||
// If there's a failure, remove the script from the global index to force
|
||||
// a reload attempt the next time it's accessed
|
||||
SCRIPTS.delete(url);
|
||||
reject();
|
||||
};
|
||||
const onScriptLoad = () => {
|
||||
removeScript(script);
|
||||
clearTimeout(timeout);
|
||||
if (globalKey && window[globalKey]) {
|
||||
return resolve(window[globalKey]);
|
||||
} else if (globalKey && !window[globalKey]) {
|
||||
return reject();
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.async = 1;
|
||||
script.type = 'text/javascript';
|
||||
script.onload = onScriptLoad;
|
||||
script.onerror = onScriptError;
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
const timeout = setTimeout(onScriptError, SCRIPT_LOAD_TIMEOUT);
|
||||
});
|
||||
}
|
||||
|
||||
function ensure(url) {
|
||||
let scriptPromise = SCRIPTS.get(url);
|
||||
if (!scriptPromise) {
|
||||
scriptPromise = loadNewScript(url);
|
||||
SCRIPTS.set(url, scriptPromise);
|
||||
return scriptPromise;
|
||||
}
|
||||
return scriptPromise;
|
||||
}
|
||||
|
||||
function register(url, key) {
|
||||
REGISTRY.set(url, key);
|
||||
}
|
||||
|
||||
export default {
|
||||
ensure,
|
||||
register,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/ScriptLoaderUtils.js
|
274
509bba0_unpacked/discord_app/utils/SearchUtils.js
Executable file
274
509bba0_unpacked/discord_app/utils/SearchUtils.js
Executable file
|
@ -0,0 +1,274 @@
|
|||
/* @flow */
|
||||
|
||||
import lodash from 'lodash';
|
||||
import QueryTokenizer from '../lib/QueryTokenizer';
|
||||
import SearchTokens from '../lib/SearchTokens';
|
||||
import SnowflakeUtils from './SnowflakeUtils';
|
||||
import {SearchTokenTypes, SearchPopoutModes, IS_SEARCH_ANSWER_TOKEN, IS_SEARCH_FILTER_TOKEN} from '../Constants';
|
||||
import i18n from '../i18n';
|
||||
import type {Token} from '../lib/QueryTokenizer';
|
||||
import type {ResultsGroup, CursorScope, SearchPopoutMode} from '../flow/Client';
|
||||
|
||||
export const SearchOptionAnswers = {
|
||||
[SearchTokens[SearchTokenTypes.FILTER_FROM].key]: i18n.Messages.SEARCH_ANSWER_FROM,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_MENTIONS].key]: i18n.Messages.SEARCH_ANSWER_MENTIONS,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_HAS].key]: i18n.Messages.SEARCH_ANSWER_HAS,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_BEFORE].key]: i18n.Messages.SEARCH_ANSWER_DATE,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_ON].key]: i18n.Messages.SEARCH_ANSWER_DATE,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_AFTER].key]: i18n.Messages.SEARCH_ANSWER_DATE,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_IN].key]: i18n.Messages.SEARCH_ANSWER_IN,
|
||||
// TODO: Uncomment when the backend is ready
|
||||
// [SearchTokens[SearchTokenTypes.FILTER_LINK_FROM].key]: i18n.Messages.SEARCH_ANSWER_LINK_FROM,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_FILE_TYPE].key]: i18n.Messages.SEARCH_ANSWER_FILE_TYPE,
|
||||
[SearchTokens[SearchTokenTypes.FILTER_FILE_NAME].key]: i18n.Messages.SEARCH_ANSWER_FILE_NAME,
|
||||
};
|
||||
|
||||
export const ShowDatePicker: {[key: ?string]: boolean} = {
|
||||
[SearchTokenTypes.FILTER_BEFORE]: true,
|
||||
[SearchTokenTypes.FILTER_AFTER]: true,
|
||||
[SearchTokenTypes.FILTER_ON]: true,
|
||||
};
|
||||
|
||||
function getQueryKey(type) {
|
||||
const tokenDefinition = SearchTokens[type];
|
||||
let queryKey = tokenDefinition ? tokenDefinition.queryKey : null;
|
||||
if (!queryKey) {
|
||||
queryKey = 'content';
|
||||
}
|
||||
return queryKey;
|
||||
}
|
||||
|
||||
export function getSearchQueryFromTokens(tokens: Array<Token>) {
|
||||
const query: {[key: string]: any} = {};
|
||||
|
||||
tokens.forEach(token => {
|
||||
const {type} = token;
|
||||
// Don't include filters in search query
|
||||
if (IS_SEARCH_FILTER_TOKEN.test(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special answer handling
|
||||
switch (type) {
|
||||
case SearchTokenTypes.ANSWER_BEFORE:
|
||||
case SearchTokenTypes.ANSWER_ON:
|
||||
case SearchTokenTypes.ANSWER_AFTER:
|
||||
const start = token.getData('start');
|
||||
const end = token.getData('end');
|
||||
if (start) {
|
||||
query['min_id'] = SnowflakeUtils.getFromTimestamp(start);
|
||||
}
|
||||
if (end) {
|
||||
query['max_id'] = SnowflakeUtils.getFromTimestamp(end);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const queryKey = getQueryKey(type);
|
||||
if (!Array.isArray(query[queryKey])) {
|
||||
query[queryKey] = [];
|
||||
}
|
||||
const values: Array<string> = query[queryKey];
|
||||
switch (type) {
|
||||
case SearchTokenTypes.ANSWER_USERNAME_FROM:
|
||||
case SearchTokenTypes.ANSWER_USERNAME_MENTIONS:
|
||||
values.push(token.getData('user').id);
|
||||
break;
|
||||
// FIXME: Either add this feature to the backend or remove it
|
||||
// case SearchTokenTypes.ANSWER_LINK_FROM:
|
||||
// query[queryKey].push(token.getMatch(1));
|
||||
// break;
|
||||
case SearchTokenTypes.ANSWER_FILE_TYPE:
|
||||
values.push(token.getMatch(1));
|
||||
break;
|
||||
case SearchTokenTypes.ANSWER_FILE_NAME:
|
||||
values.push(token.getMatch(1));
|
||||
break;
|
||||
case SearchTokenTypes.ANSWER_IN:
|
||||
values.push(token.getData('channel').id);
|
||||
break;
|
||||
default:
|
||||
values.push(token.getFullMatch().trim());
|
||||
}
|
||||
});
|
||||
|
||||
if (query.content) {
|
||||
query.content = query.content.join(' ').trim();
|
||||
if (!query.content) {
|
||||
delete query.content;
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export function getNonTokenQuery(tokens: Array<Token>) {
|
||||
return tokens
|
||||
.map(token => (token.type === QueryTokenizer.NON_TOKEN_TYPE ? token.getFullMatch() : ''))
|
||||
.join(' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function getSelectionScope(tokens: Array<Token>, focusOffset: number, anchorOffset: number) {
|
||||
let previousToken;
|
||||
let nextToken;
|
||||
const currentToken = tokens.find((token, index) => {
|
||||
if (
|
||||
focusOffset >= token.start &&
|
||||
focusOffset <= token.end &&
|
||||
anchorOffset >= token.start &&
|
||||
anchorOffset <= token.end
|
||||
) {
|
||||
if (tokens[index + 1]) {
|
||||
nextToken = tokens[index + 1];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
previousToken = token;
|
||||
return false;
|
||||
});
|
||||
|
||||
// If we can't find a currentToken it means we have a selection that breaks
|
||||
// outside a token and therefore can't be properly scoped
|
||||
if (!currentToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
previousToken,
|
||||
currentToken,
|
||||
nextToken,
|
||||
focusOffset,
|
||||
anchorOffset,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAutocompleteMode(cursorScope: CursorScope, tokens: Array<Token>): SearchPopoutMode {
|
||||
cursorScope = cursorScope || {};
|
||||
const {currentToken, nextToken, previousToken} = cursorScope;
|
||||
if (tokens.length === 0) {
|
||||
return {
|
||||
type: SearchPopoutModes.EMPTY,
|
||||
filter: null,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
if (!currentToken) {
|
||||
return {
|
||||
type: SearchPopoutModes.FILTER_ALL,
|
||||
filter: null,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (IS_SEARCH_FILTER_TOKEN.test(currentToken.type)) {
|
||||
if (!nextToken || nextToken.type === QueryTokenizer.NON_TOKEN_TYPE) {
|
||||
return {
|
||||
type: SearchPopoutModes.FILTER,
|
||||
filter: currentToken.type,
|
||||
token: nextToken,
|
||||
};
|
||||
}
|
||||
if (nextToken && !IS_SEARCH_ANSWER_TOKEN.test(nextToken.type)) {
|
||||
return {
|
||||
type: SearchPopoutModes.FILTER,
|
||||
filter: currentToken.type,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
currentToken.type === QueryTokenizer.NON_TOKEN_TYPE &&
|
||||
(previousToken && IS_SEARCH_FILTER_TOKEN.test(previousToken.type))
|
||||
) {
|
||||
return {
|
||||
type: SearchPopoutModes.FILTER,
|
||||
filter: previousToken.type,
|
||||
token: currentToken,
|
||||
};
|
||||
}
|
||||
|
||||
let token;
|
||||
if (currentToken.type === QueryTokenizer.NON_TOKEN_TYPE) {
|
||||
token = currentToken;
|
||||
}
|
||||
// May introduce weird autocomplete behavior
|
||||
// if (nextToken && nextToken.type === QueryTokenizer.NON_TOKEN_TYPE) {
|
||||
// token = nextToken;
|
||||
// }
|
||||
|
||||
return {
|
||||
type: SearchPopoutModes.FILTER_ALL,
|
||||
filter: null,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFlattenedStringArray(results: Array<?ResultsGroup>, modeType: string) {
|
||||
let flattened = [];
|
||||
lodash(results).forEach(resultGroup => {
|
||||
if (!resultGroup || !resultGroup.results.length) {
|
||||
return;
|
||||
}
|
||||
let tokenType = resultGroup.group;
|
||||
flattened = flattened.concat(
|
||||
resultGroup.results.map(result => {
|
||||
let resultText = result.text;
|
||||
if (modeType === SearchPopoutModes.FILTER_ALL) {
|
||||
tokenType = result.group || tokenType;
|
||||
const token = SearchTokens[tokenType];
|
||||
if (token && token.key) {
|
||||
resultText = `${token.key} ${resultText}`;
|
||||
}
|
||||
}
|
||||
return resultText;
|
||||
})
|
||||
);
|
||||
});
|
||||
// Filter out null results - mostly for safety
|
||||
return flattened.filter(result => result);
|
||||
}
|
||||
|
||||
export function getTotalResults(results: Array<?ResultsGroup>): number {
|
||||
return results.reduce((count, resultsGroup) => {
|
||||
if (!resultsGroup) {
|
||||
return count;
|
||||
}
|
||||
return resultsGroup.results.length + count;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function getQueryFromTokens(tokens: ?Array<Token>) {
|
||||
if (!tokens) {
|
||||
return '';
|
||||
}
|
||||
return tokens.map(token => token.getFullMatch()).join('');
|
||||
}
|
||||
|
||||
const tokenizer = new QueryTokenizer();
|
||||
lodash(SearchTokens).forOwn((rule, type) => tokenizer.addRule({type, ...rule}));
|
||||
|
||||
export function tokenizeQuery(query: string) {
|
||||
return tokenizer.tokenize(query);
|
||||
}
|
||||
|
||||
export function clearTokenCache() {
|
||||
return tokenizer.clearCache();
|
||||
}
|
||||
|
||||
export function showDatePicker(filter: ?string): ?boolean {
|
||||
return ShowDatePicker[filter];
|
||||
}
|
||||
|
||||
export function filterHasAnswer(token: Token, nextToken: ?Token) {
|
||||
const isFilter = IS_SEARCH_FILTER_TOKEN.test(token.type);
|
||||
if ((!nextToken && isFilter) || (nextToken && isFilter && !IS_SEARCH_ANSWER_TOKEN.test(nextToken.type))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/SearchUtils.js
|
34
509bba0_unpacked/discord_app/utils/SnowflakeUtils.js
Executable file
34
509bba0_unpacked/discord_app/utils/SnowflakeUtils.js
Executable file
|
@ -0,0 +1,34 @@
|
|||
/* @flow */
|
||||
|
||||
import Long from 'long';
|
||||
|
||||
const DISCORD_EPOCH = 1420070400000;
|
||||
const SHIFT = 22;
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Since message ids are Snowflakes you can extract the UNIX timestamp.
|
||||
*/
|
||||
extractTimestamp(id: ?string): number {
|
||||
if (id == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return Long.fromString(id, true).shiftRight(SHIFT).add(Long.fromNumber(DISCORD_EPOCH)).toNumber();
|
||||
}
|
||||
},
|
||||
|
||||
getFromTimestamp(timestamp: number): string {
|
||||
const epochTime = timestamp - DISCORD_EPOCH;
|
||||
|
||||
if (epochTime <= 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return Long.fromNumber(epochTime).shiftLeft(SHIFT).toString();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/SnowflakeUtils.js
|
111
509bba0_unpacked/discord_app/utils/SoundUtils.js
Executable file
111
509bba0_unpacked/discord_app/utils/SoundUtils.js
Executable file
|
@ -0,0 +1,111 @@
|
|||
/* @flow */
|
||||
|
||||
import path from 'path';
|
||||
import NativeUtils from './NativeUtils';
|
||||
import StreamerModeStore from '../stores/StreamerModeStore';
|
||||
import {NativeFeatures} from '../Constants';
|
||||
|
||||
class NativeSoundPlayer {
|
||||
nextSoundId: number = 1;
|
||||
voe = NativeUtils.getVoiceEngine();
|
||||
|
||||
play(name, volume = 1, loop = false) {
|
||||
let filename = path.join('sounds', `${name}.wav`);
|
||||
if (!NativeUtils.supportsFeature(NativeFeatures.VOICE_RELATIVE_SOUNDS)) {
|
||||
const {resourcesPath} = NativeUtils.require('process', true);
|
||||
filename = path.join(resourcesPath, filename);
|
||||
}
|
||||
|
||||
if (NativeUtils.supportsFeature(NativeFeatures.VOICE_SOUND_STOP_LOOP)) {
|
||||
this.voe.playSound(this.nextSoundId, filename, loop, volume);
|
||||
} else {
|
||||
this.voe.playSound(filename, volume);
|
||||
}
|
||||
|
||||
return this.nextSoundId++;
|
||||
}
|
||||
|
||||
stop(id) {
|
||||
if (NativeUtils.supportsFeature(NativeFeatures.VOICE_SOUND_STOP_LOOP)) {
|
||||
this.voe.stopSound(id);
|
||||
}
|
||||
}
|
||||
|
||||
setVolume(id, volume) {
|
||||
if (NativeUtils.supportsFeature(NativeFeatures.VOICE_SOUND_STOP_LOOP)) {
|
||||
this.voe.setSoundVolume(id, volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NativeSound {
|
||||
player: NativeSoundPlayer;
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
_volume: number;
|
||||
_id: number;
|
||||
|
||||
constructor(name: string, volume: number) {
|
||||
this.player = new NativeSoundPlayer();
|
||||
this.name = name;
|
||||
this._volume = volume;
|
||||
this._id = 0;
|
||||
}
|
||||
|
||||
get volume(): number {
|
||||
return this._volume;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
this._volume = value;
|
||||
this.player.setVolume(this._id, this._volume);
|
||||
}
|
||||
|
||||
loop() {
|
||||
this._id = this._id || this.player.play(this.name, this._volume, true);
|
||||
}
|
||||
|
||||
play() {
|
||||
this.stop();
|
||||
this._id = this.player.play(this.name, this._volume, false);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._id > 0) {
|
||||
this.player.stop(this._id);
|
||||
this._id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let AudioSound;
|
||||
if (__IOS__) {
|
||||
AudioSound = require('./ios/SoundUtils').iOSAudioSound;
|
||||
} else if (__SDK__ || NativeUtils.isOSX()) {
|
||||
AudioSound = NativeSound;
|
||||
} else {
|
||||
AudioSound = require('./web/SoundUtils').WebAudioSound;
|
||||
}
|
||||
|
||||
export interface Sound {
|
||||
play(): void,
|
||||
loop(): void,
|
||||
stop(): void,
|
||||
}
|
||||
|
||||
export function createSound(name: string, volume?: number = 1): Sound {
|
||||
return new AudioSound(name, volume);
|
||||
}
|
||||
|
||||
export function playSound(name: string, volume?: number = 1): ?Sound {
|
||||
if (StreamerModeStore.disableSounds) return;
|
||||
const sound = createSound(name, volume);
|
||||
sound.play();
|
||||
return sound;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/SoundUtils.js
|
13
509bba0_unpacked/discord_app/utils/StringUtils.js
Executable file
13
509bba0_unpacked/discord_app/utils/StringUtils.js
Executable file
|
@ -0,0 +1,13 @@
|
|||
// flow
|
||||
|
||||
export function upperCaseFirstChar(str: string): string {
|
||||
if (str == null || typeof str !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/StringUtils.js
|
17
509bba0_unpacked/discord_app/utils/StripeUtils.js
Executable file
17
509bba0_unpacked/discord_app/utils/StripeUtils.js
Executable file
|
@ -0,0 +1,17 @@
|
|||
import ScriptLoaderUtils from './ScriptLoaderUtils';
|
||||
import {PaymentSettings} from '../Constants';
|
||||
|
||||
const STRIPE_JS_URL = 'https://js.stripe.com/v2/';
|
||||
ScriptLoaderUtils.register(STRIPE_JS_URL, 'Stripe');
|
||||
|
||||
export function ensureStripeIsLoaded() {
|
||||
return ScriptLoaderUtils.ensure(STRIPE_JS_URL).then(Stripe => {
|
||||
Stripe.setPublishableKey(PaymentSettings.STRIPE.KEY);
|
||||
return Stripe;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/StripeUtils.js
|
23
509bba0_unpacked/discord_app/utils/StylesheetUtils.js
Executable file
23
509bba0_unpacked/discord_app/utils/StylesheetUtils.js
Executable file
|
@ -0,0 +1,23 @@
|
|||
/* @flow */
|
||||
|
||||
import {upperCaseFirstChar} from './StringUtils';
|
||||
|
||||
export function getClass(Styles: Object, name: string, ...args: Array<?string>): string {
|
||||
const modes = args.reduce((acc, mode) => acc + upperCaseFirstChar(mode), '');
|
||||
const className = `${name}${modes}`;
|
||||
const hashedClassName = Styles[className];
|
||||
|
||||
if (hashedClassName == null) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`Class doesn't exist:`, name, className);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return hashedClassName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/StylesheetUtils.js
|
175
509bba0_unpacked/discord_app/utils/SystemMessageUtils.js
Executable file
175
509bba0_unpacked/discord_app/utils/SystemMessageUtils.js
Executable file
|
@ -0,0 +1,175 @@
|
|||
/* @flow */
|
||||
import i18n from '../i18n';
|
||||
import {MessageTypes} from '../Constants';
|
||||
import AuthenticationStore from '../stores/AuthenticationStore';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
import UserStore from '../stores/UserStore';
|
||||
import UserSettingsStore from '../stores/UserSettingsStore';
|
||||
import {astToString} from './MarkupASTUtils';
|
||||
import NicknameUtils from './NicknameUtils';
|
||||
import SnowflakeUtils from './SnowflakeUtils';
|
||||
import type {Message} from '../flow/Server';
|
||||
|
||||
function noop() {}
|
||||
|
||||
const getGuildWelcomeMessages = (): Array<Object> => [
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_01,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_02,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_03,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_04,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_05,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_06,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_07,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_08,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_09,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_10,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_11,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_12,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_13,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_14,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_15,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_16,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_17,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_18,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_19,
|
||||
i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN_20,
|
||||
];
|
||||
|
||||
function getSystemMessageGuildMemberJoined(messageId: string): Object {
|
||||
const welcomeMessageList = getGuildWelcomeMessages();
|
||||
const messageIndex = SnowflakeUtils.extractTimestamp(messageId) % welcomeMessageList.length;
|
||||
return UserSettingsStore.locale.startsWith('en')
|
||||
? welcomeMessageList[messageIndex]
|
||||
: i18n.Messages.SYSTEM_MESSAGE_GUILD_MEMBER_JOIN;
|
||||
}
|
||||
function stringify(message: Message): ?string {
|
||||
const otherUser = message.mentions[0] && UserStore.getUser(message.mentions[0]);
|
||||
const channelId = message.channel_id;
|
||||
const username = NicknameUtils.getName(null, channelId, message.author);
|
||||
|
||||
switch (message.type) {
|
||||
case MessageTypes.RECIPIENT_ADD:
|
||||
if (!otherUser) return;
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_RECIPIENT_ADD.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
otherUsername: NicknameUtils.getName(null, channelId, otherUser),
|
||||
otherUsernameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
case MessageTypes.RECIPIENT_REMOVE:
|
||||
if (!otherUser) return;
|
||||
const user = message.author;
|
||||
// Remove self from group
|
||||
if (user.id === otherUser.id) {
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_RECIPIENT_REMOVE_SELF.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Remove a member
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_RECIPIENT_REMOVE.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
otherUsername: NicknameUtils.getName(null, channelId, otherUser),
|
||||
otherUsernameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
case MessageTypes.CALL:
|
||||
const {call} = message;
|
||||
if (call && call.participants.indexOf(AuthenticationStore.getId()) === -1) {
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_CALL_STARTED.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
case MessageTypes.CHANNEL_NAME_CHANGE:
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_CHANNEL_NAME_CHANGE.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
channelName: message.content,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
case MessageTypes.CHANNEL_ICON_CHANGE:
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_CHANNEL_ICON_CHANGE.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
case MessageTypes.CHANNEL_PINNED_MESSAGE:
|
||||
return astToString(
|
||||
i18n.Messages.SYSTEM_MESSAGE_PINNED_MESSAGE_NO_CTA.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
case MessageTypes.GUILD_MEMBER_JOIN:
|
||||
const guild = GuildStore.getGuild(message.channel_id);
|
||||
if (guild) {
|
||||
const welcomeFlavour = getSystemMessageGuildMemberJoined(message.id);
|
||||
return astToString(
|
||||
welcomeFlavour.format(
|
||||
{
|
||||
username,
|
||||
usernameOnClick: noop,
|
||||
guildName: guild.name,
|
||||
guildNameOnClick: noop,
|
||||
},
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return message.content;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
stringify,
|
||||
getSystemMessageGuildMemberJoined,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/SystemMessageUtils.js
|
21
509bba0_unpacked/discord_app/utils/TimerUtils.js
Executable file
21
509bba0_unpacked/discord_app/utils/TimerUtils.js
Executable file
|
@ -0,0 +1,21 @@
|
|||
let TimerUtils;
|
||||
if (__IOS__) {
|
||||
TimerUtils = require('./ios/TimerUtils');
|
||||
} else {
|
||||
TimerUtils = {
|
||||
setInterval(func, delay) {
|
||||
return window.setInterval(func, delay);
|
||||
},
|
||||
|
||||
clearInterval(key) {
|
||||
window.clearInterval(key);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default TimerUtils;
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/TimerUtils.js
|
38
509bba0_unpacked/discord_app/utils/TypingUtils.js
Executable file
38
509bba0_unpacked/discord_app/utils/TypingUtils.js
Executable file
|
@ -0,0 +1,38 @@
|
|||
/* @flow */
|
||||
|
||||
import TypingActionCreators from '../actions/TypingActionCreators';
|
||||
import TypingStore from '../stores/TypingStore';
|
||||
import {TYPING_TIMEOUT, MAX_TYPING_USERS} from '../Constants';
|
||||
|
||||
let currentChannelId = null;
|
||||
let nextSend = null;
|
||||
|
||||
export default {
|
||||
typing(channelId: string) {
|
||||
if (currentChannelId === channelId && nextSend != null && nextSend > Date.now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentChannelId = channelId;
|
||||
|
||||
const count = TypingStore.getCount(channelId);
|
||||
if (count > MAX_TYPING_USERS) {
|
||||
nextSend = Date.now() + TYPING_TIMEOUT;
|
||||
return;
|
||||
}
|
||||
|
||||
TypingActionCreators.sendTyping(channelId);
|
||||
nextSend = Date.now() + TYPING_TIMEOUT * 0.8;
|
||||
},
|
||||
|
||||
clear(channelId: string) {
|
||||
if (currentChannelId === channelId) {
|
||||
nextSend = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/TypingUtils.js
|
18
509bba0_unpacked/discord_app/utils/UserSettingsUtils.js
Executable file
18
509bba0_unpacked/discord_app/utils/UserSettingsUtils.js
Executable file
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import GuildAvailabilityStore from '../stores/GuildAvailabilityStore';
|
||||
import UserSettingsStore from '../stores/UserSettingsStore';
|
||||
import GuildStore from '../stores/GuildStore';
|
||||
|
||||
export function getSanitizedRestrictedGuilds(): Array<string> {
|
||||
let restrictedGuilds = UserSettingsStore.restrictedGuilds;
|
||||
if (GuildAvailabilityStore.totalUnavailableGuilds === 0) {
|
||||
restrictedGuilds = restrictedGuilds.filter(guildId => GuildStore.getGuild(guildId) != null);
|
||||
}
|
||||
return restrictedGuilds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/UserSettingsUtils.js
|
25
509bba0_unpacked/discord_app/utils/web/AutoUpdateUtils.js
Executable file
25
509bba0_unpacked/discord_app/utils/web/AutoUpdateUtils.js
Executable file
|
@ -0,0 +1,25 @@
|
|||
import {ActionTypes} from '../../Constants';
|
||||
import AutoUpdateStore from '../../stores/AutoUpdateStore';
|
||||
import AutoUpdateManager from '../../lib/AutoUpdateManager';
|
||||
|
||||
const autoUpdate = new AutoUpdateManager();
|
||||
window.autoUpdate = autoUpdate;
|
||||
|
||||
export default {
|
||||
checkForUpdates() {
|
||||
if (AutoUpdateStore.getState() === ActionTypes.UPDATE_NOT_AVAILABLE) {
|
||||
autoUpdate.checkForUpdates();
|
||||
}
|
||||
},
|
||||
|
||||
quitAndInstall() {
|
||||
if (AutoUpdateStore.getState() === ActionTypes.UPDATE_DOWNLOADED) {
|
||||
autoUpdate.quitAndInstall();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/AutoUpdateUtils.js
|
24
509bba0_unpacked/discord_app/utils/web/AvatarUtils.js
Executable file
24
509bba0_unpacked/discord_app/utils/web/AvatarUtils.js
Executable file
|
@ -0,0 +1,24 @@
|
|||
const DEFAULT_AVATARS = [
|
||||
require('../../images/default_avatar_0.png'),
|
||||
require('../../images/default_avatar_1.png'),
|
||||
require('../../images/default_avatar_2.png'),
|
||||
require('../../images/default_avatar_3.png'),
|
||||
require('../../images/default_avatar_4.png'),
|
||||
];
|
||||
|
||||
const DEFAULT_CHANNEL_ICON = require('../../images/icon-group.svg');
|
||||
|
||||
const BOT_AVATARS = {
|
||||
clyde: require('../../images/clyde.png'),
|
||||
};
|
||||
|
||||
export default {
|
||||
DEFAULT_AVATARS,
|
||||
BOT_AVATARS,
|
||||
DEFAULT_CHANNEL_ICON,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/AvatarUtils.js
|
24
509bba0_unpacked/discord_app/utils/web/EmojiUtils.js
Executable file
24
509bba0_unpacked/discord_app/utils/web/EmojiUtils.js
Executable file
|
@ -0,0 +1,24 @@
|
|||
import twemoji from 'twemoji';
|
||||
|
||||
function getURL(unicodeSurrogates) {
|
||||
// twmoji makes these unicode characters into emojis, but they shouldn't be
|
||||
if (['™', '©', '®'].indexOf(unicodeSurrogates) > -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return require(`twemoji/2/svg/${twemoji.convert.toCodePoint(unicodeSurrogates)}.svg`);
|
||||
} catch (err) {
|
||||
console.warn(err, 'no emoji for', unicodeSurrogates);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getURL,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/EmojiUtils.js
|
94
509bba0_unpacked/discord_app/utils/web/ErrorUtils.js
Executable file
94
509bba0_unpacked/discord_app/utils/web/ErrorUtils.js
Executable file
|
@ -0,0 +1,94 @@
|
|||
import Raven from 'raven-js';
|
||||
import NativeUtils from './NativeUtils';
|
||||
|
||||
export const DSN = 'https://fa97a90475514c03a42f80cd36d147c4@sentry.io/140984';
|
||||
|
||||
// https://gist.github.com/oliviertassinari/114792721df8b7ef2cf2ecf3e4dea621
|
||||
// https://github.com/getsentry/raven-js/issues/530
|
||||
function filterThrottle({maxBudgetMinute, maxBudgetHour}) {
|
||||
// Count the number of events sent over the last period of time.
|
||||
const count = {
|
||||
minute: {
|
||||
slot: 0,
|
||||
budgetUsed: 0,
|
||||
},
|
||||
hour: {
|
||||
slot: 0,
|
||||
budgetUsed: 0,
|
||||
},
|
||||
};
|
||||
|
||||
return () => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
const minuteSlot = Math.round(timestamp / 1000 / 60);
|
||||
const hourSlot = Math.round(timestamp / 1000 / 60 / 60);
|
||||
|
||||
// We are on a new minute slot.
|
||||
if (count.minute.slot !== minuteSlot) {
|
||||
count.minute.slot = minuteSlot;
|
||||
count.minute.budgetUsed = 0;
|
||||
}
|
||||
|
||||
// We are on a new hour slot.
|
||||
if (count.hour.slot !== hourSlot) {
|
||||
count.hour.slot = hourSlot;
|
||||
count.hour.budgetUsed = 0;
|
||||
}
|
||||
|
||||
// Check minute usage
|
||||
if (count.minute.budgetUsed < maxBudgetMinute) {
|
||||
count.minute.budgetUsed++;
|
||||
|
||||
// Check hour usage
|
||||
if (count.hour.budgetUsed < maxBudgetHour) {
|
||||
count.hour.budgetUsed++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export function hasSuspiciousCode(data: any): boolean {
|
||||
return (
|
||||
// People who modify the Discord client usually use jQuery.
|
||||
window.jQuery != null ||
|
||||
window.$ != null ||
|
||||
// Explicitly check for the existence of BetterDiscord.
|
||||
window.BetterDiscord != null ||
|
||||
// Discord has a defined set of <script> and <link> tags.
|
||||
document.querySelectorAll('script,link').length !== 7 ||
|
||||
// Any exception that has no real stacktrace is useless and probably injected.
|
||||
data.exception.values.every(exception => exception.stacktrace.frames.length === 1)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateNativeReporter(user: {id: string, username: string, email: ?string} = {}) {
|
||||
if (!NativeUtils.embedded) return;
|
||||
NativeUtils.updateCrashReporter({
|
||||
// eslint-disable-next-line camelcase
|
||||
user_id: user.id || '',
|
||||
username: user.username || '',
|
||||
email: user.email || '',
|
||||
});
|
||||
}
|
||||
|
||||
export function crash() {
|
||||
throw new Error('crash');
|
||||
}
|
||||
|
||||
const shouldSendCallback = filterThrottle({
|
||||
maxBudgetMinute: 1,
|
||||
maxBudgetHour: 3,
|
||||
});
|
||||
|
||||
Raven.setShouldSendCallback(data => {
|
||||
return process.env.NODE_ENV === 'production' && !hasSuspiciousCode(data) && shouldSendCallback();
|
||||
});
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/ErrorUtils.js
|
38
509bba0_unpacked/discord_app/utils/web/FullScreenUtils.js
Executable file
38
509bba0_unpacked/discord_app/utils/web/FullScreenUtils.js
Executable file
|
@ -0,0 +1,38 @@
|
|||
export function requestFullScreen(node) {
|
||||
if (node.requestFullscreen) {
|
||||
node.requestFullscreen();
|
||||
} else if (node.webkitRequestFullscreen) {
|
||||
node.webkitRequestFullscreen();
|
||||
} else if (node.mozRequestFullScreen) {
|
||||
node.mozRequestFullScreen();
|
||||
} else if (node.msRequestFullscreen) {
|
||||
node.msRequestFullscreen();
|
||||
} else {
|
||||
console.warn('Fullscreen API is not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
export function exitFullScreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else {
|
||||
console.warn('Fullscreen API is not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
export function isFullScreen() {
|
||||
return document.fullscreen || document.webkitIsFullScreen;
|
||||
}
|
||||
|
||||
export const fullScreenChangeEvents = ['fullscreenchange', 'webkitfullscreenchange'];
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/FullScreenUtils.js
|
454
509bba0_unpacked/discord_app/utils/web/KeyboardUtils.js
Executable file
454
509bba0_unpacked/discord_app/utils/web/KeyboardUtils.js
Executable file
|
@ -0,0 +1,454 @@
|
|||
import NativeUtils from '../NativeUtils';
|
||||
|
||||
export const TYPE_MOUSE_BUTTON = 1;
|
||||
export const TYPE_KEYBOARD_KEY = 0;
|
||||
export const TYPE_KEYBOARD_MODIFIER_KEY = 2;
|
||||
export const TYPE_GAMEPAD_BUTTON = 3;
|
||||
|
||||
let KEY_TO_CODE;
|
||||
|
||||
if (NativeUtils.isLinux()) {
|
||||
KEY_TO_CODE = {
|
||||
escape: 9,
|
||||
f1: 67,
|
||||
f2: 68,
|
||||
f3: 69,
|
||||
f4: 70,
|
||||
f5: 71,
|
||||
f6: 72,
|
||||
f7: 73,
|
||||
f8: 74,
|
||||
f9: 75,
|
||||
f10: 76,
|
||||
f11: 95,
|
||||
f12: 96,
|
||||
// 'f13': 0,
|
||||
f14: 107,
|
||||
f15: 78,
|
||||
f16: 127,
|
||||
// 'f17': 0,
|
||||
// 'f18': 0,
|
||||
// 'f19': 0,
|
||||
'`': 49,
|
||||
'1': 10,
|
||||
'2': 11,
|
||||
'3': 12,
|
||||
'4': 13,
|
||||
'5': 14,
|
||||
'6': 15,
|
||||
'7': 16,
|
||||
'8': 17,
|
||||
'9': 18,
|
||||
'0': 19,
|
||||
'-': 20,
|
||||
'=': 21,
|
||||
backspace: 22,
|
||||
tab: 23,
|
||||
q: 24,
|
||||
w: 25,
|
||||
e: 26,
|
||||
r: 27,
|
||||
t: 28,
|
||||
y: 29,
|
||||
u: 30,
|
||||
i: 31,
|
||||
o: 32,
|
||||
p: 33,
|
||||
'[': 34,
|
||||
']': 35,
|
||||
'\\': 51,
|
||||
capslock: 66,
|
||||
a: 38,
|
||||
s: 39,
|
||||
d: 40,
|
||||
f: 41,
|
||||
g: 42,
|
||||
h: 43,
|
||||
j: 44,
|
||||
k: 45,
|
||||
l: 46,
|
||||
';': 47,
|
||||
// eslint-disable-next-line quotes
|
||||
"'": 48,
|
||||
enter: 36,
|
||||
'left shift': 50,
|
||||
z: 52,
|
||||
x: 53,
|
||||
c: 54,
|
||||
v: 55,
|
||||
b: 56,
|
||||
n: 57,
|
||||
m: 58,
|
||||
',': 59,
|
||||
'.': 60,
|
||||
'/': 61,
|
||||
'right shift': 62,
|
||||
'left ctrl': 37,
|
||||
'left alt': 64,
|
||||
'left meta': 133,
|
||||
space: 65,
|
||||
'right meta': 134,
|
||||
'right alt': 108,
|
||||
'right ctrl': 105,
|
||||
menu: 135,
|
||||
numlock: 77,
|
||||
'numpad =': 125,
|
||||
'numpad /': 106,
|
||||
'numpad *': 63,
|
||||
'numpad 7': 79,
|
||||
'numpad 8': 80,
|
||||
'numpad 9': 81,
|
||||
'numpad -': 82,
|
||||
'numpad 4': 83,
|
||||
'numpad 5': 84,
|
||||
'numpad 6': 85,
|
||||
'numpad +': 86,
|
||||
'numpad 1': 87,
|
||||
'numpad 2': 88,
|
||||
'numpad 3': 89,
|
||||
'numpad enter': 104,
|
||||
'numpad 0': 90,
|
||||
'numpad .': 91,
|
||||
home: 110,
|
||||
pageup: 112,
|
||||
end: 115,
|
||||
pagedown: 117,
|
||||
insert: 118,
|
||||
delete: 119,
|
||||
left: 113,
|
||||
right: 114,
|
||||
down: 116,
|
||||
up: 111,
|
||||
|
||||
sleep: 150,
|
||||
back: 166,
|
||||
forward: 167,
|
||||
'home key': 180,
|
||||
favorites: 164,
|
||||
email: 163,
|
||||
|
||||
play: 172,
|
||||
stop: 174,
|
||||
'vol down': 122,
|
||||
'vol up': 123,
|
||||
'track back': 173,
|
||||
'track skip': 171,
|
||||
};
|
||||
} else if (NativeUtils.isOSX()) {
|
||||
KEY_TO_CODE = {
|
||||
a: 4,
|
||||
s: 22,
|
||||
d: 7,
|
||||
f: 9,
|
||||
h: 11,
|
||||
g: 10,
|
||||
z: 29,
|
||||
x: 27,
|
||||
c: 6,
|
||||
v: 25,
|
||||
b: 5,
|
||||
q: 20,
|
||||
w: 26,
|
||||
e: 8,
|
||||
r: 21,
|
||||
y: 28,
|
||||
t: 23,
|
||||
'1': 30,
|
||||
'2': 31,
|
||||
'3': 32,
|
||||
'4': 33,
|
||||
'6': 35,
|
||||
'5': 34,
|
||||
'=': 46,
|
||||
'9': 38,
|
||||
'7': 36,
|
||||
'-': 45,
|
||||
'8': 37,
|
||||
'0': 39,
|
||||
']': 48,
|
||||
o: 18,
|
||||
u: 24,
|
||||
'[': 47,
|
||||
i: 12,
|
||||
p: 19,
|
||||
l: 15,
|
||||
j: 13,
|
||||
// eslint-disable-next-line quotes
|
||||
"'": 52,
|
||||
k: 14,
|
||||
';': 51,
|
||||
'\\': 49,
|
||||
',': 54,
|
||||
'/': 56,
|
||||
n: 17,
|
||||
m: 16,
|
||||
'.': 55,
|
||||
'`': 53,
|
||||
'numpad .': 99,
|
||||
'numpad *': 85,
|
||||
'numpad +': 87,
|
||||
'numpad clear': 83,
|
||||
'numpad /': 84,
|
||||
'numpad enter': 88,
|
||||
'numpad -': 86,
|
||||
'numpad =': 103,
|
||||
'numpad 0': 98,
|
||||
'numpad 1': 89,
|
||||
'numpad 2': 90,
|
||||
'numpad 3': 91,
|
||||
'numpad 4': 92,
|
||||
'numpad 5': 93,
|
||||
'numpad 6': 94,
|
||||
'numpad 7': 95,
|
||||
'numpad 8': 96,
|
||||
'numpad 9': 97,
|
||||
enter: 40,
|
||||
tab: 43,
|
||||
space: 44,
|
||||
backspace: 42,
|
||||
esc: 41,
|
||||
meta: 227,
|
||||
shift: 225,
|
||||
capslock: 57,
|
||||
alt: 226,
|
||||
ctrl: 224,
|
||||
'right shift': 229,
|
||||
'right alt': 230,
|
||||
'right ctrl': 228,
|
||||
'right meta': 231,
|
||||
f17: 108,
|
||||
f18: 109,
|
||||
f19: 110,
|
||||
f20: 111,
|
||||
f5: 62,
|
||||
f6: 63,
|
||||
f7: 64,
|
||||
f3: 60,
|
||||
f8: 65,
|
||||
f9: 66,
|
||||
f11: 68,
|
||||
f13: 104,
|
||||
f16: 107,
|
||||
f14: 105,
|
||||
f10: 67,
|
||||
f12: 69,
|
||||
f15: 106,
|
||||
home: 74,
|
||||
pageup: 75,
|
||||
del: 76,
|
||||
f4: 61,
|
||||
end: 77,
|
||||
f2: 59,
|
||||
pagedown: 78,
|
||||
f1: 58,
|
||||
left: 80,
|
||||
right: 79,
|
||||
down: 81,
|
||||
up: 82,
|
||||
};
|
||||
} else if (NativeUtils.isWindows()) {
|
||||
KEY_TO_CODE = {
|
||||
a: 65,
|
||||
s: 83,
|
||||
d: 68,
|
||||
f: 70,
|
||||
h: 72,
|
||||
g: 71,
|
||||
z: 90,
|
||||
x: 88,
|
||||
c: 67,
|
||||
v: 86,
|
||||
b: 66,
|
||||
q: 81,
|
||||
w: 87,
|
||||
e: 69,
|
||||
r: 82,
|
||||
y: 89,
|
||||
t: 84,
|
||||
'1': 49,
|
||||
'2': 50,
|
||||
'3': 51,
|
||||
'4': 52,
|
||||
'6': 54,
|
||||
'5': 53,
|
||||
'=': 187,
|
||||
'9': 57,
|
||||
'7': 55,
|
||||
'-': 189,
|
||||
'8': 56,
|
||||
'0': 48,
|
||||
']': 221,
|
||||
o: 79,
|
||||
u: 85,
|
||||
'[': 219,
|
||||
i: 73,
|
||||
p: 80,
|
||||
l: 76,
|
||||
j: 74,
|
||||
k: 75,
|
||||
';': 186,
|
||||
',': 188,
|
||||
'/': 191,
|
||||
n: 78,
|
||||
m: 77,
|
||||
'.': 190,
|
||||
'numpad .': 110,
|
||||
'numpad *': 106,
|
||||
'numpad +': 107,
|
||||
'numpad clear': 144,
|
||||
'numpad /': 111,
|
||||
'numpad -': 109,
|
||||
'numpad =': 226,
|
||||
'numpad 0': 96,
|
||||
'numpad 1': 97,
|
||||
'numpad 2': 98,
|
||||
'numpad 3': 99,
|
||||
'numpad 4': 100,
|
||||
'numpad 5': 101,
|
||||
'numpad 6': 102,
|
||||
'numpad 7': 103,
|
||||
'numpad 8': 104,
|
||||
'numpad 9': 105,
|
||||
enter: 13,
|
||||
tab: 9,
|
||||
space: 32,
|
||||
backspace: 8,
|
||||
esc: 27,
|
||||
meta: 91,
|
||||
shift: 160,
|
||||
capslock: 20,
|
||||
alt: 164,
|
||||
ctrl: 162,
|
||||
'right shift': 161,
|
||||
'right alt': 165,
|
||||
'right ctrl': 163,
|
||||
'right meta': 93,
|
||||
f17: 128,
|
||||
f18: 129,
|
||||
f19: 130,
|
||||
//'f20': null,
|
||||
f5: 116,
|
||||
f6: 117,
|
||||
f7: 118,
|
||||
f3: 114,
|
||||
f8: 119,
|
||||
f9: 120,
|
||||
f11: 122,
|
||||
f13: 124,
|
||||
f16: 127,
|
||||
f14: 125,
|
||||
f10: 121,
|
||||
f12: 123,
|
||||
f15: 126,
|
||||
home: 36,
|
||||
pageup: 33,
|
||||
del: 46,
|
||||
f4: 115,
|
||||
end: 35,
|
||||
f2: 113,
|
||||
pagedown: 34,
|
||||
f1: 112,
|
||||
left: 37,
|
||||
right: 39,
|
||||
down: 40,
|
||||
up: 38,
|
||||
insert: 45,
|
||||
break: 19,
|
||||
'scroll lock': 145,
|
||||
'print screen': 44,
|
||||
rewind: 177,
|
||||
play: 179,
|
||||
'fast forward': 176,
|
||||
|
||||
// on Stan's test, he got:
|
||||
'`': 192,
|
||||
'\\': 220,
|
||||
// eslint-disable-next-line quotes
|
||||
"'": 222,
|
||||
|
||||
// on Chris's machine these are:
|
||||
// '`': 223,
|
||||
// '\\': 222,
|
||||
// '\'': 192
|
||||
// Adding an additional mapping for 223 below to catch that case, but we'd need additional info to disambiguate the other cases.
|
||||
};
|
||||
}
|
||||
|
||||
const CODE_TO_KEY = {};
|
||||
for (const key in KEY_TO_CODE) {
|
||||
if (KEY_TO_CODE.hasOwnProperty(key)) {
|
||||
CODE_TO_KEY[KEY_TO_CODE[key]] = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (!NativeUtils.isOSX()) {
|
||||
CODE_TO_KEY[223] = '`';
|
||||
}
|
||||
|
||||
export function codeToKey(code) {
|
||||
return CODE_TO_KEY[code] || 'unknown';
|
||||
}
|
||||
|
||||
export function toString(combo, pretty = false) {
|
||||
if (NativeUtils.embedded) {
|
||||
if (typeof combo !== 'object') {
|
||||
combo = '';
|
||||
} else {
|
||||
combo = combo
|
||||
.map(([type, code]) => {
|
||||
if (type === TYPE_KEYBOARD_KEY || type === TYPE_KEYBOARD_MODIFIER_KEY) {
|
||||
return CODE_TO_KEY[code] || `UNK${code}`;
|
||||
} else if (type === TYPE_MOUSE_BUTTON) {
|
||||
return `mouse${code}`;
|
||||
} else if (type === TYPE_GAMEPAD_BUTTON) {
|
||||
return `gamepad${code}`;
|
||||
} else {
|
||||
return `dev${type},${code}`;
|
||||
}
|
||||
})
|
||||
.filter(key => key != null)
|
||||
.join('+');
|
||||
}
|
||||
}
|
||||
if (pretty) {
|
||||
if (navigator.appVersion.indexOf('Mac OS X') !== -1) {
|
||||
// http://apple.stackexchange.com/questions/55727/where-can-i-find-the-unicode-symbols-for-mac-functional-keys-command-shift-e
|
||||
let parts = combo.toUpperCase().split('+');
|
||||
[
|
||||
['META', '⌘'],
|
||||
['RIGHT META', 'RIGHT ⌘'],
|
||||
['SHIFT', '⇧'],
|
||||
['RIGHT SHIFT', 'RIGHT ⇧'],
|
||||
['ALT', '⌥'],
|
||||
['RIGHT ALT', 'RIGHT ⌥'],
|
||||
['CTRL', '⌃'],
|
||||
['RIGHT CTRL', 'RIGHT ⌃'],
|
||||
['ENTER', '↵'],
|
||||
['BACKSPACE', '⌫'],
|
||||
['DEL', '⌦'],
|
||||
['ESC', '⎋'],
|
||||
['PAGEUP', '⇞'],
|
||||
['PAGEDOWN', '⇟'],
|
||||
['UP', '↑'],
|
||||
['DOWN', '↓'],
|
||||
['LEFT', '←'],
|
||||
['RIGHT', '→'],
|
||||
['HOME', '↖'],
|
||||
['END', '↘'],
|
||||
['TAB', '⇥'],
|
||||
['SPACE', '␣'],
|
||||
].forEach(([key, repl]) => {
|
||||
parts = parts.map(part => (key === part ? repl : part));
|
||||
});
|
||||
combo = parts.join(' + ');
|
||||
} else {
|
||||
combo = combo.replace(/\+/g, ' + ').toUpperCase();
|
||||
}
|
||||
}
|
||||
return combo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/KeyboardUtils.js
|
19
509bba0_unpacked/discord_app/utils/web/MFAInterceptionUtils.js
Executable file
19
509bba0_unpacked/discord_app/utils/web/MFAInterceptionUtils.js
Executable file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import ModalActionCreators from '../../actions/ModalActionCreators';
|
||||
import MFAConfirmModal from '../../components/MFAConfirmModal';
|
||||
|
||||
export function showModal(handleSubmitCode, handleEarlyClose, confirmModalProps) {
|
||||
return ModalActionCreators.push(props =>
|
||||
<MFAConfirmModal
|
||||
handleSubmit={handleSubmitCode}
|
||||
handleEarlyClose={handleEarlyClose}
|
||||
{...confirmModalProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/MFAInterceptionUtils.js
|
119
509bba0_unpacked/discord_app/utils/web/MarkupUtils.js
Executable file
119
509bba0_unpacked/discord_app/utils/web/MarkupUtils.js
Executable file
|
@ -0,0 +1,119 @@
|
|||
import SimpleMarkdown from 'simple-markdown';
|
||||
import EmojiStore from '../../stores/EmojiStore';
|
||||
import UnicodeEmojis from '../../lib/UnicodeEmojis';
|
||||
import AvatarUtils from '../AvatarUtils';
|
||||
import EmojiUtils from '../EmojiUtils';
|
||||
import {flattenAst, constrainAst} from '../MarkupASTUtils';
|
||||
|
||||
function createRules(defaultRules) {
|
||||
return {
|
||||
...defaultRules,
|
||||
|
||||
s: {
|
||||
order: defaultRules.u.order,
|
||||
match: SimpleMarkdown.inlineRegex(/^~~([\s\S]+?)~~(?!_)/),
|
||||
parse: defaultRules.u.parse,
|
||||
},
|
||||
|
||||
url: {
|
||||
...defaultRules.url,
|
||||
|
||||
match: SimpleMarkdown.inlineRegex(/^((https?|steam):\/\/[^\s<]+[^<.,:;"')\]\s])/),
|
||||
},
|
||||
|
||||
emoji: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return UnicodeEmojis.EMOJI_NAME_AND_DIVERSITY_RE.exec(source);
|
||||
},
|
||||
|
||||
parse([content, name]) {
|
||||
const surrogate = UnicodeEmojis.convertNameToSurrogate(name);
|
||||
if (!surrogate) {
|
||||
return {
|
||||
type: 'text',
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: `:${name}:`,
|
||||
surrogate,
|
||||
src: EmojiUtils.getURL(surrogate),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
customEmoji: {
|
||||
order: SimpleMarkdown.defaultRules.text.order,
|
||||
|
||||
match(source) {
|
||||
return /^<:(\w+):(\d+)>/.exec(source);
|
||||
},
|
||||
|
||||
parse([_, name, emojiId], __, {guildId}) {
|
||||
const emote = EmojiStore.getDisambiguatedEmojiContext(guildId).getById(emojiId);
|
||||
const requiresColons = !emote || emote.require_colons;
|
||||
|
||||
// If the emoji is found, show the latest (or disambiguated) name.
|
||||
if (emote) {
|
||||
name = emote.name;
|
||||
}
|
||||
|
||||
return {
|
||||
emojiId,
|
||||
name: requiresColons ? `:${name}:` : name,
|
||||
src: AvatarUtils.getEmojiURL({id: emojiId}),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
text: {
|
||||
...defaultRules.text,
|
||||
|
||||
parse(capture, nestedParse, state) {
|
||||
if (state.nested) {
|
||||
return {
|
||||
content: capture[0],
|
||||
};
|
||||
} else {
|
||||
return nestedParse(UnicodeEmojis.translateSurrogatesToInlineEmoji(capture[0]), {...state, nested: true});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!__SDK__) {
|
||||
const originalCreateRules = createRules;
|
||||
const reactCreateRules = require('./MarkupUtils.react').createRules;
|
||||
createRules = rules => reactCreateRules(originalCreateRules(rules));
|
||||
}
|
||||
|
||||
export default {
|
||||
createRules,
|
||||
|
||||
parserFor(rules, returnTree = false) {
|
||||
const parser = SimpleMarkdown.parserFor(rules);
|
||||
const output = SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, 'react'));
|
||||
return function(str = '', inline = true, state = {}, postProcess = null) {
|
||||
if (!inline) {
|
||||
str += '\n\n';
|
||||
}
|
||||
let tree = parser(str, {inline, ...state});
|
||||
tree = flattenAst(tree);
|
||||
tree = constrainAst(tree);
|
||||
if (postProcess) {
|
||||
tree = postProcess(tree, inline);
|
||||
}
|
||||
|
||||
return returnTree || __SDK__ ? tree : output(tree);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/MarkupUtils.js
|
288
509bba0_unpacked/discord_app/utils/web/MarkupUtils.react.js
Executable file
288
509bba0_unpacked/discord_app/utils/web/MarkupUtils.react.js
Executable file
|
@ -0,0 +1,288 @@
|
|||
import SimpleMarkdown from 'simple-markdown';
|
||||
import React from 'react';
|
||||
import highlight from 'highlight.js';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {ContextMenuTypes, Routes, ChannelTypes} from '../../Constants';
|
||||
|
||||
import RouterUtils from '../RouterUtils';
|
||||
import EmojiStore from '../../stores/EmojiStore';
|
||||
import UserStore from '../../stores/UserStore';
|
||||
import ChannelStore from '../../stores/ChannelStore';
|
||||
import RelationshipStore from '../../stores/RelationshipStore';
|
||||
import SelectedChannelStore from '../../stores/SelectedChannelStore';
|
||||
|
||||
import InstantInviteActionCreators from '../../actions/InstantInviteActionCreators';
|
||||
|
||||
import {findInvite} from '../InstantInviteUtils';
|
||||
import {astToString} from '../MarkupASTUtils';
|
||||
|
||||
import UserPopout from '../../components/UserPopout';
|
||||
import ContextMenu from '../../components/common/ContextMenu';
|
||||
import Popout from '../../components/common/Popout';
|
||||
import Pill from '../../components/common/Pill';
|
||||
import Tooltip from '../../components/common/Tooltip';
|
||||
import UserContextMenu from '../../components/contextmenus/UserContextMenu';
|
||||
|
||||
import MaskedLink from '../../components/common/MaskedLink';
|
||||
|
||||
import '../../styles/code.styl';
|
||||
|
||||
/**
|
||||
* Avoids running output if node content is already a string.
|
||||
*/
|
||||
function smartOutput(node, output, state) {
|
||||
return typeof node.content === 'string' ? node.content : output(node.content, state);
|
||||
}
|
||||
|
||||
function createRules(defaultRules) {
|
||||
return {
|
||||
...defaultRules,
|
||||
|
||||
s: {
|
||||
...defaultRules.s,
|
||||
|
||||
react(node, output, state) {
|
||||
return <s key={state.key}>{output(node.content, state)}</s>;
|
||||
},
|
||||
},
|
||||
|
||||
highlight: {
|
||||
order: defaultRules.text.order,
|
||||
|
||||
match() {
|
||||
return null;
|
||||
},
|
||||
|
||||
react(node, output, state) {
|
||||
return <span key={state.key} className="highlight">{node.content}</span>;
|
||||
},
|
||||
},
|
||||
|
||||
paragraph: {
|
||||
...defaultRules.paragraph,
|
||||
|
||||
react(node, output, state) {
|
||||
return <p key={state.key}>{output(node.content, state)}</p>;
|
||||
},
|
||||
},
|
||||
|
||||
link: {
|
||||
...defaultRules.link,
|
||||
|
||||
react(node, output, state) {
|
||||
const code = findInvite(node.target);
|
||||
|
||||
let onClick;
|
||||
const content = output(node.content, state);
|
||||
const title = node.title || astToString(node.content);
|
||||
|
||||
let forceMaskedLink = false;
|
||||
const selectedChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
||||
if (selectedChannel != null && selectedChannel.type === ChannelTypes.DM) {
|
||||
forceMaskedLink = !RelationshipStore.isFriend(selectedChannel.getRecipientId());
|
||||
}
|
||||
|
||||
if (code != null) {
|
||||
onClick = e => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
InstantInviteActionCreators.acceptInviteAndTransitionToInviteChannel(code, 'Markdown Link');
|
||||
};
|
||||
}
|
||||
|
||||
return title != node.target || forceMaskedLink
|
||||
? <MaskedLink
|
||||
key={state.key}
|
||||
title={title}
|
||||
href={SimpleMarkdown.sanitizeUrl(node.target)}
|
||||
onConfirm={title != node.target ? () => window.open(node.target, '_blank') : null}>
|
||||
{content}
|
||||
</MaskedLink>
|
||||
: <a
|
||||
key={state.key}
|
||||
title={title}
|
||||
href={SimpleMarkdown.sanitizeUrl(node.target)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={onClick}>
|
||||
{content}
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
|
||||
emoji: {
|
||||
...defaultRules.emoji,
|
||||
|
||||
react(node, output, state) {
|
||||
if (!node.src) {
|
||||
return <span key={state.key}>{node.surrogate}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip key={state.key} text={node.name} delay={750} position={Tooltip.TOP}>
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames('emoji', {jumboable: node.jumboable})}
|
||||
alt={node.name}
|
||||
src={node.src}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
customEmoji: {
|
||||
...defaultRules.customEmoji,
|
||||
|
||||
react(node, output, {key, guildId}) {
|
||||
return (
|
||||
<Tooltip
|
||||
key={key}
|
||||
text={() => {
|
||||
// Attempt to show the latest disambiguated name.
|
||||
const emoji = EmojiStore.getDisambiguatedEmojiContext(guildId).getById(node.emojiId);
|
||||
if (emoji) {
|
||||
const requiresColons = !emoji || emoji.require_colons;
|
||||
return requiresColons ? `:${emoji.name}:` : emoji.name;
|
||||
}
|
||||
return node.name;
|
||||
}}
|
||||
delay={750}
|
||||
position={Tooltip.TOP}>
|
||||
<img
|
||||
draggable={false}
|
||||
className={classNames('emoji', {jumboable: node.jumboable})}
|
||||
alt={node.name}
|
||||
src={node.src}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
mention: {
|
||||
...defaultRules.mention,
|
||||
|
||||
handleUserContextMenu(user, channelId, guildId, event) {
|
||||
ContextMenu.openContextMenu(event, props =>
|
||||
<UserContextMenu
|
||||
{...props}
|
||||
type={ContextMenuTypes.USER_CHANNEL_MENTION}
|
||||
user={user}
|
||||
channelId={channelId}
|
||||
guildId={guildId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
react(node, output, state) {
|
||||
const props = {
|
||||
className: 'mention',
|
||||
};
|
||||
|
||||
let Component = 'span';
|
||||
if (node.color) {
|
||||
Component = Pill;
|
||||
props.color = node.color;
|
||||
}
|
||||
|
||||
// If it's a role mention
|
||||
if (!node.userId) {
|
||||
return (
|
||||
<Component key={state.key} {...props}>
|
||||
{output(node.content, state)}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
const user = UserStore.getUser(node.userId);
|
||||
const selectedChannel = ChannelStore.getChannel(node.channelId);
|
||||
const guildId = selectedChannel ? selectedChannel.getGuildId() : null;
|
||||
if (guildId) {
|
||||
props.onContextMenu = this.handleUserContextMenu.bind(this, user, node.channelId, guildId);
|
||||
}
|
||||
return (
|
||||
<Popout
|
||||
key={state.key}
|
||||
closeOnScroll={false}
|
||||
render={props => <UserPopout {...props} user={user} guildId={guildId} channelId={node.channelId} />}
|
||||
position={Popout.RIGHT}>
|
||||
<Component {...props}>
|
||||
{output(node.content, state)}
|
||||
</Component>
|
||||
</Popout>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
channel: {
|
||||
...defaultRules.channel,
|
||||
|
||||
handleClick(guildId, channelId) {
|
||||
if (guildId != null && channelId != null) {
|
||||
RouterUtils.transitionTo(Routes.CHANNEL(guildId, channelId));
|
||||
}
|
||||
},
|
||||
|
||||
react(node, output, state) {
|
||||
return (
|
||||
<span key={state.key} onClick={this.handleClick.bind(this, node.guildId, node.channelId)} className="mention">
|
||||
{output(node.content, state)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
inlineCode: {
|
||||
...defaultRules.inlineCode,
|
||||
|
||||
react(node, output, state) {
|
||||
return <code key={state.key} className="inline">{smartOutput(node, output, state)}</code>;
|
||||
},
|
||||
},
|
||||
|
||||
codeBlock: {
|
||||
...defaultRules.codeBlock,
|
||||
|
||||
react(node, output, state) {
|
||||
if (node.lang && highlight.getLanguage(node.lang) != null) {
|
||||
const code = highlight.highlight(node.lang, node.content, true);
|
||||
return (
|
||||
<pre key={state.key}>
|
||||
<code className={`hljs ${code.language}`} dangerouslySetInnerHTML={{__html: code.value}} />
|
||||
</pre>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<pre key={state.key}>
|
||||
<code className="hljs">
|
||||
{smartOutput(node, output, state)}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
text: {
|
||||
...defaultRules.text,
|
||||
|
||||
react(node, output, state) {
|
||||
if (typeof node.content === 'string') {
|
||||
return node.content;
|
||||
}
|
||||
return <span key={state.key}>{output(node.content, state)}</span>;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
createRules,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/MarkupUtils.react.js
|
588
509bba0_unpacked/discord_app/utils/web/NativeUtils.js
Executable file
588
509bba0_unpacked/discord_app/utils/web/NativeUtils.js
Executable file
|
@ -0,0 +1,588 @@
|
|||
// These are copied here to avoid importing Constants.
|
||||
const ELECTRON_CONFIGURE_HARDWARE_ACCELERATION = 'electron_configure_hardware_acceleration';
|
||||
const VOICE_IN_RENDERER = 'voice_in_renderer';
|
||||
const UTILS_IN_RENDERER = 'utils_in_renderer';
|
||||
const WHITE = '#ffffff';
|
||||
const STATUS_RED = '#f04747';
|
||||
|
||||
const __require = window.__require;
|
||||
const electronProcess = __require ? __require('process') : null;
|
||||
const electron = __require ? __require('electron') : null;
|
||||
const features = electron ? electron.remote.getGlobal('features') : null;
|
||||
|
||||
let clientVersion = null;
|
||||
|
||||
if (electron) {
|
||||
clientVersion = electron.remote.app.getVersion().split('.').map(i => parseInt(i));
|
||||
|
||||
const mainAppDirname = electron.remote.getGlobal('mainAppDirname');
|
||||
// this adds in the base node_modules dir, and the one in the asar
|
||||
window.module.paths = __require('module')._nodeModulePaths(mainAppDirname);
|
||||
// this adds in our native modules path
|
||||
const remotePaths = electron.remote.require('module').globalPaths;
|
||||
remotePaths.forEach(path => {
|
||||
if (path.indexOf('electron.asar') === -1) {
|
||||
window.module.paths.push(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const modulePromises = {};
|
||||
|
||||
if (electron != null) {
|
||||
electron.ipcRenderer.on('MODULE_INSTALLED', (_e, name, success) => {
|
||||
const promise = modulePromises[name];
|
||||
|
||||
if (promise == null || promise.callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
promise.callback(success);
|
||||
});
|
||||
|
||||
electron.ipcRenderer.on('TRACK_ANALYTICS_EVENT', e => {
|
||||
// We don't care about logging these anymore.
|
||||
// just commit so that they don't back up on disk.
|
||||
e.sender.send('TRACK_ANALYTICS_EVENT_COMMIT');
|
||||
});
|
||||
}
|
||||
|
||||
let taskbarBadgeTimeout = null;
|
||||
let hasRequiredVoice = false;
|
||||
|
||||
export default {
|
||||
embedded: __require != null,
|
||||
|
||||
/**
|
||||
* Access globals shared by the native client
|
||||
*/
|
||||
getGlobal(named) {
|
||||
return electron.remote.getGlobal(named);
|
||||
},
|
||||
|
||||
/**
|
||||
* Import an Electron module.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Boolean} remote
|
||||
* @return {Object}
|
||||
*/
|
||||
require(name, remote = false) {
|
||||
if (__require != null) {
|
||||
return remote ? electron.remote.require(name) : __require(name);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
requireElectron(name, remote) {
|
||||
if (remote) {
|
||||
return electron.remote[name];
|
||||
}
|
||||
return electron[name];
|
||||
},
|
||||
|
||||
requireModule(name, remote) {
|
||||
let module = null;
|
||||
try {
|
||||
module = this.require(name, remote);
|
||||
} catch (e) {}
|
||||
|
||||
return module;
|
||||
},
|
||||
|
||||
installModule(name) {
|
||||
this.send('MODULE_INSTALL', name);
|
||||
},
|
||||
|
||||
ensureModule(name) {
|
||||
let modulePromise = modulePromises[name];
|
||||
if (modulePromise == null) {
|
||||
modulePromise = modulePromises[name] = {};
|
||||
modulePromise.promise = new Promise((resolve, reject) => {
|
||||
modulePromise.callback = success => {
|
||||
modulePromise.callback = null;
|
||||
success ? resolve() : reject();
|
||||
};
|
||||
|
||||
this.installModule(name);
|
||||
});
|
||||
}
|
||||
|
||||
return modulePromise.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do any cleanup before navigating away or refreshing.
|
||||
*/
|
||||
beforeUnload() {
|
||||
this.getVoiceEngine().onInvokingContextDestroyed();
|
||||
this.getDiscordUtils().beforeUnload();
|
||||
const overlay = this.requireModule('discord_overlay', true);
|
||||
if (overlay && overlay.reset) {
|
||||
overlay.reset();
|
||||
}
|
||||
|
||||
this.requireElectron('powerMonitor', true).removeAllListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Integer} eventId
|
||||
* @param {String} shortcut key to trigger event. A string or array of scan codes
|
||||
* @param {Function} callback when event fires
|
||||
* @param {Object} events values for keys keyup, keydown, blurred, or focused
|
||||
*/
|
||||
inputEventRegister(eventId, shortcut, callback, events) {
|
||||
if (!Array.isArray(shortcut)) {
|
||||
shortcut = shortcut.toJS();
|
||||
}
|
||||
|
||||
this.getDiscordUtils().inputEventRegister(parseInt(eventId), shortcut, callback, events);
|
||||
},
|
||||
|
||||
inputEventUnregister(eventId) {
|
||||
this.getDiscordUtils().inputEventUnregister(parseInt(eventId));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get amount of time current user has been idle in milliseconds.
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
getIdleMilliseconds(callback) {
|
||||
this.getDiscordUtils().getIdleMilliseconds(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Observe the OS process list for a defined list of processes.
|
||||
* Callback anytime the list of running processes changes
|
||||
*
|
||||
* @param {Array<Object>} processes
|
||||
* @param {Function} callback
|
||||
*/
|
||||
setObservedGamesCallback(processes, callback) {
|
||||
this.getDiscordUtils().setObservedGamesCallback(processes, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Observe the OS process list for potential games.
|
||||
* Callback anytime the list of changes
|
||||
*
|
||||
* @param {Function} callback
|
||||
*/
|
||||
setCandidateGamesCallback(callback) {
|
||||
this.getDiscordUtils().setCandidateGamesCallback(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop observing the OS process list. Clears out the callback given to setCandidateGamesCallback.
|
||||
*/
|
||||
clearCandidateGamesCallback() {
|
||||
this.getDiscordUtils().clearCandidateGamesCallback();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set list of processes to add or block
|
||||
* @param {Array<Object>} overrideList
|
||||
*/
|
||||
setGameCandidateOverrides(overrideList) {
|
||||
this.getDiscordUtils().setGameCandidateOverrides(overrideList);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether or not the operating system is in a mode where the user
|
||||
* should be receiving notifications. If this is false, we shouldn't
|
||||
* be showing notifications to the user.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldDisplayNotifications() {
|
||||
this._shouldDisplayNotifications =
|
||||
this._shouldDisplayNotifications || this.getDiscordUtils().shouldDisplayNotifications;
|
||||
return this._shouldDisplayNotifications != null ? this._shouldDisplayNotifications() : true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the VoiceEngine instance.
|
||||
*
|
||||
* @return {VoiceEngine}
|
||||
*/
|
||||
getVoiceEngine() {
|
||||
hasRequiredVoice = true;
|
||||
return this.require('discord_voice', !this.supportsFeature(VOICE_IN_RENDERER));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get discord_utils
|
||||
*/
|
||||
getDiscordUtils() {
|
||||
if (!hasRequiredVoice) {
|
||||
this.getVoiceEngine();
|
||||
}
|
||||
return this.require('discord_utils', !this.supportsFeature(UTILS_IN_RENDERER));
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the badge on the app icon on platforms which support it.
|
||||
*
|
||||
* @param {String} badge
|
||||
*/
|
||||
setBadge(badge) {
|
||||
let text = `${badge}`;
|
||||
|
||||
if (this.platform === 'darwin') {
|
||||
electron.remote.app.dock.setBadge(text);
|
||||
} else if (this.platform === 'win32' || this.platform == 'linux') {
|
||||
clearTimeout(taskbarBadgeTimeout);
|
||||
const renderBadge = () => {
|
||||
const font = 'Whitney';
|
||||
const badgeSize = 40;
|
||||
let fontSize = 64;
|
||||
let textX = 100;
|
||||
let textY = 120;
|
||||
|
||||
if (text.length > 2) {
|
||||
text = '99+';
|
||||
fontSize = 42;
|
||||
textX = 100;
|
||||
textY = 120;
|
||||
} else if (text.length > 1) {
|
||||
fontSize = 54;
|
||||
textX = 100;
|
||||
textY = 120;
|
||||
}
|
||||
|
||||
if (document.fonts.check(`bold ${fontSize}px ${font}`)) {
|
||||
const icon = new Image();
|
||||
|
||||
icon.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = 140;
|
||||
canvas.height = 140;
|
||||
|
||||
// Draw base icon
|
||||
ctx.drawImage(icon, 0, 0, canvas.height, canvas.width);
|
||||
|
||||
if (text.length > 0) {
|
||||
// Draw badge background
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = STATUS_RED;
|
||||
ctx.ellipse(canvas.width - badgeSize, canvas.height - badgeSize, badgeSize, badgeSize, 0, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
// Draw badge text
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = WHITE;
|
||||
ctx.font = `bold ${fontSize}px "${font}"`;
|
||||
ctx.fillText(text, textX, textY);
|
||||
|
||||
this.send('BADGE_IS_ENABLED');
|
||||
} else {
|
||||
this.send('BADGE_IS_DISABLED');
|
||||
}
|
||||
|
||||
const badgeDataURL = canvas.toDataURL();
|
||||
this.send('SET_ICON', {icon: badgeDataURL});
|
||||
};
|
||||
|
||||
icon.src = require(`../../images/discord-icons/discord-${this.releaseChannel}.png`);
|
||||
} else {
|
||||
clearTimeout(taskbarBadgeTimeout);
|
||||
taskbarBadgeTimeout = setTimeout(renderBadge, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
renderBadge();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Bounces the dock icon on OS X.
|
||||
*/
|
||||
bounceDock(type) {
|
||||
if (this.embedded) {
|
||||
const app = electron.remote.app;
|
||||
if (app.dock != null) {
|
||||
const bounceId = app.dock.bounce(type);
|
||||
return () => {
|
||||
app.dock.cancelBounce(bounceId);
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The name of the current operating system.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
get platform() {
|
||||
return this.embedded ? electronProcess.platform : '';
|
||||
},
|
||||
|
||||
/**
|
||||
* The name of the current native app release channel. E.g., development or canary.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
get releaseChannel() {
|
||||
if (!this.embedded) {
|
||||
return '';
|
||||
}
|
||||
let rc = electron.remote.getGlobal('releaseChannel');
|
||||
if (rc) {
|
||||
return rc;
|
||||
}
|
||||
rc = this.require('./singleInstance', true).releaseChannel;
|
||||
if (rc) {
|
||||
return rc;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* The version of the native client, as an array of ints
|
||||
*/
|
||||
get version() {
|
||||
return clientVersion;
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy text to Clipboard but only if currently in the native app.
|
||||
* If you pass text, put into clipboard. Otherwise use system copy.
|
||||
* Needed for better contextmenus; see NativeLinkGroup vs NativeCopyItem
|
||||
*
|
||||
* @param {String} text
|
||||
*/
|
||||
copy(text) {
|
||||
if (this.embedded) {
|
||||
if (text) {
|
||||
electron.remote.clipboard.writeText(text);
|
||||
} else {
|
||||
this._getCurrentWindow().webContents.copy();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cut text to Clipboard but only if currently in the native app.
|
||||
*/
|
||||
cut() {
|
||||
if (this.embedded) {
|
||||
this._getCurrentWindow().webContents.cut();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Paste text from Clipboard but only if currently in the native app.
|
||||
*/
|
||||
paste() {
|
||||
if (this.embedded) {
|
||||
this._getCurrentWindow().webContents.paste();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open an external page with passed URL but only if currently in the native app.
|
||||
*/
|
||||
openExternal(href) {
|
||||
if (this.embedded) {
|
||||
electron.remote.shell.openExternal(href);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind to events from the Electron parent process.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
*/
|
||||
on(event, callback) {
|
||||
electron.ipcRenderer.on(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send event to the Electron parent process.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Array<Object>} args
|
||||
*/
|
||||
send(event, ...args) {
|
||||
electron.ipcRenderer.send(event, ...args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts or stops flashing the window to attract user's attention.
|
||||
*
|
||||
* @param {Boolean} flag
|
||||
*/
|
||||
flashFrame(flag) {
|
||||
const currentWindow = this._getCurrentWindow();
|
||||
if (currentWindow != null) {
|
||||
currentWindow.flashFrame && currentWindow.flashFrame(!currentWindow.isFocused() && flag);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Minimize the app.
|
||||
*
|
||||
* @param {boolean} [main]
|
||||
*/
|
||||
minimize(main = false) {
|
||||
const w = main ? this._getMainWindow() : this._getCurrentWindow();
|
||||
if (w != null) {
|
||||
w.minimize();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore the app.
|
||||
*
|
||||
* @param {boolean} [main]
|
||||
*/
|
||||
restore(main = false) {
|
||||
const w = main ? this._getMainWindow() : this._getCurrentWindow();
|
||||
w.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Maximize the app.
|
||||
*
|
||||
* @param {boolean} [main]
|
||||
*/
|
||||
maximize(main = false) {
|
||||
const w = main ? this._getMainWindow() : this._getCurrentWindow();
|
||||
if (w.isMaximized()) {
|
||||
w.unmaximize();
|
||||
} else {
|
||||
w.maximize();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus the app.
|
||||
*
|
||||
* @param {boolean} [main]
|
||||
*/
|
||||
focus(main = false) {
|
||||
const w = main ? this._getMainWindow() : this._getCurrentWindow();
|
||||
w.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Fullscreen the app in OSX
|
||||
*/
|
||||
fullscreen() {
|
||||
const currentWindow = this._getCurrentWindow();
|
||||
currentWindow.setFullScreen(!currentWindow.isFullScreen());
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current Window.
|
||||
*
|
||||
* @return {BrowserWindow}
|
||||
*/
|
||||
_getCurrentWindow() {
|
||||
return electron.remote.getCurrentWindow();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get main Window.
|
||||
*
|
||||
* @return {BrowserWindow}
|
||||
*/
|
||||
_getMainWindow() {
|
||||
const mainWindowId = electron.remote.getGlobal('mainWindowId');
|
||||
return electron.remote.BrowserWindow.fromId(mainWindowId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the app.
|
||||
* On OSX this will only close it to the dock.
|
||||
*/
|
||||
close() {
|
||||
if (this.isOSX()) {
|
||||
const Menu = electron.remote.Menu;
|
||||
Menu.sendActionToFirstResponder('hide:');
|
||||
} else {
|
||||
this._getCurrentWindow().close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Purge as much memory as you can.
|
||||
*/
|
||||
purgeMemory() {
|
||||
if (!this.embedded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webFrame = this.requireElectron('webFrame');
|
||||
if (webFrame.clearCache) {
|
||||
webFrame.clearCache();
|
||||
} else if (electronProcess.purgeMemory) {
|
||||
electronProcess.purgeMemory();
|
||||
} else {
|
||||
global.process.binding('discord_utils').purgeMemory();
|
||||
}
|
||||
},
|
||||
|
||||
updateCrashReporter(metadata) {
|
||||
const extra = {...electron.remote.getGlobal('crashReporterMetadata'), ...metadata};
|
||||
this.send('UPDATE_CRASH_REPORT', metadata);
|
||||
electron.crashReporter.start({
|
||||
productName: 'Discord',
|
||||
companyName: 'Discord Inc.',
|
||||
submitURL: 'http://crash.discordapp.com:1127/post',
|
||||
autoSubmit: true,
|
||||
ignoreSystemCrashHandler: false,
|
||||
extra,
|
||||
});
|
||||
},
|
||||
|
||||
flushDNSCache() {
|
||||
if (!this.embedded) return;
|
||||
const session = this.requireElectron('session', true);
|
||||
if (!session) return;
|
||||
const defaultSession = session.defaultSession;
|
||||
if (!defaultSession || !defaultSession.clearHostResolverCache) return;
|
||||
defaultSession.clearHostResolverCache();
|
||||
},
|
||||
|
||||
supportsFeature(feature) {
|
||||
return features && features.supports(feature);
|
||||
},
|
||||
|
||||
getEnableHardwareAcceleration() {
|
||||
if (!this.supportsFeature(ELECTRON_CONFIGURE_HARDWARE_ACCELERATION)) {
|
||||
return true;
|
||||
}
|
||||
return this.require('./GPUSettings', true).getEnableHardwareAcceleration();
|
||||
},
|
||||
|
||||
setEnableHardwareAcceleration(enableHardwareAcceleration) {
|
||||
if (this.supportsFeature(ELECTRON_CONFIGURE_HARDWARE_ACCELERATION)) {
|
||||
this.require('./GPUSettings', true).setEnableHardwareAcceleration(enableHardwareAcceleration);
|
||||
}
|
||||
},
|
||||
|
||||
setZoomFactor(factor) {
|
||||
if (!this.embedded) {
|
||||
return false;
|
||||
}
|
||||
const webFrame = this.requireElectron('webFrame');
|
||||
if (webFrame.setZoomFactor) {
|
||||
webFrame.setZoomFactor(factor / 100);
|
||||
}
|
||||
},
|
||||
|
||||
setFocused(focused) {
|
||||
this.getDiscordUtils().setFocused(focused);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/NativeUtils.js
|
25
509bba0_unpacked/discord_app/utils/web/NetworkUtils.js
Executable file
25
509bba0_unpacked/discord_app/utils/web/NetworkUtils.js
Executable file
|
@ -0,0 +1,25 @@
|
|||
export default {
|
||||
addOnlineCallback(callback) {
|
||||
window.addEventListener('online', callback);
|
||||
},
|
||||
removeOnlineCallback(callback) {
|
||||
window.removeEventListener('online', callback);
|
||||
},
|
||||
addOfflineCallback(callback) {
|
||||
window.addEventListener('offline', callback);
|
||||
},
|
||||
removeOfflineCallback(callback) {
|
||||
window.removeEventListener('offline', callback);
|
||||
},
|
||||
isOnline() {
|
||||
const navigatorOnline = navigator.onLine;
|
||||
// Assume true if browser does not support online/offline events.
|
||||
if (navigatorOnline === undefined) return true;
|
||||
return navigatorOnline;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/NetworkUtils.js
|
182
509bba0_unpacked/discord_app/utils/web/NotificationUtils.js
Executable file
182
509bba0_unpacked/discord_app/utils/web/NotificationUtils.js
Executable file
|
@ -0,0 +1,182 @@
|
|||
/* @flow */
|
||||
|
||||
import NativeUtils from '../NativeUtils';
|
||||
import StreamerModeStore from '../../stores/StreamerModeStore';
|
||||
import {playSound} from '../SoundUtils';
|
||||
import platform from 'platform';
|
||||
|
||||
// Older than Windows 10.
|
||||
const isWindows = NativeUtils.embedded && NativeUtils.isWindows();
|
||||
const isOlderWindows = isWindows && parseFloat(NativeUtils.require('os').release()) < 10;
|
||||
|
||||
// Prior to Creator's Update some users had freezing when closing notifications.
|
||||
let supportsClose = true;
|
||||
if (isWindows && !isOlderWindows) {
|
||||
const [major, , build] = NativeUtils.require('os').release().split('.');
|
||||
supportsClose = parseInt(major) > 10 || parseInt(build) >= 15063;
|
||||
}
|
||||
|
||||
// These browsers did not automatically close notifications and required manual interaction.
|
||||
const requireInteraction =
|
||||
isOlderWindows ||
|
||||
(platform.name === 'Chrome' && parseFloat(platform.version, 10) < 47) ||
|
||||
(platform.name === 'Firefox' && parseFloat(platform.version, 10) < 52);
|
||||
|
||||
if (NativeUtils.embedded && NativeUtils.isWindows()) {
|
||||
function handleFocus() {
|
||||
NativeUtils.flashFrame(false);
|
||||
}
|
||||
window.addEventListener('focus', handleFocus);
|
||||
NativeUtils.on('MAIN_WINDOW_FOCUS', handleFocus);
|
||||
}
|
||||
|
||||
let Notification = window.Notification;
|
||||
if (isOlderWindows) {
|
||||
const notifications = {};
|
||||
|
||||
NativeUtils.on('NOTIFICATION_CLICK', (e, notificationId) => {
|
||||
const notification = notifications[notificationId];
|
||||
if (notification != null) {
|
||||
notification.onclick();
|
||||
notification.close();
|
||||
}
|
||||
});
|
||||
|
||||
NativeUtils.send('NOTIFICATIONS_CLEAR');
|
||||
|
||||
Notification = class {
|
||||
static permission: string = 'granted';
|
||||
static _id: number = 0;
|
||||
|
||||
id: number = Notification._id++;
|
||||
title: string;
|
||||
body: string;
|
||||
icon: ?string;
|
||||
|
||||
onshow: Function = function() {};
|
||||
onclick: Function = function() {};
|
||||
onclose: Function = function() {};
|
||||
|
||||
//noinspection JSUnusedGlobalSymbols
|
||||
static requestPermission(callback: Function) {
|
||||
callback();
|
||||
}
|
||||
|
||||
constructor(title: string, {body, icon}: {body: string, icon?: ?string}) {
|
||||
this.title = title;
|
||||
this.body = body;
|
||||
this.icon = icon;
|
||||
|
||||
setImmediate(() => this.onshow());
|
||||
|
||||
notifications[this.id] = this;
|
||||
NativeUtils.send('NOTIFICATION_SHOW', {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
body: this.body,
|
||||
icon: this.icon,
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (notifications[this.id] != null) {
|
||||
delete notifications[this.id];
|
||||
NativeUtils.send('NOTIFICATION_CLOSE', this.id);
|
||||
this.onclose();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function requestPermission(callback: (granted: boolean) => void) {
|
||||
if (Notification != null) {
|
||||
Notification.requestPermission(() => {
|
||||
if (callback != null) {
|
||||
callback(hasPermission());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermission() {
|
||||
return Notification != null ? Notification.permission === 'granted' : false;
|
||||
}
|
||||
|
||||
export type NotificationOptions = {
|
||||
tag?: string,
|
||||
sound?: string,
|
||||
volume?: number,
|
||||
onClick?: Function,
|
||||
};
|
||||
|
||||
interface ClosableNotification {
|
||||
close(): void,
|
||||
}
|
||||
|
||||
function showNotification(
|
||||
icon: ?string,
|
||||
title: string,
|
||||
body: string,
|
||||
options: NotificationOptions
|
||||
): ?ClosableNotification {
|
||||
if (options.sound) {
|
||||
playSound(options.sound, options.volume || 1);
|
||||
}
|
||||
|
||||
if (
|
||||
StreamerModeStore.disableNotifications ||
|
||||
!hasPermission() ||
|
||||
(NativeUtils.embedded && !NativeUtils.shouldDisplayNotifications())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = (options && options.tag) || null;
|
||||
|
||||
const notificationOptions = {
|
||||
icon,
|
||||
body,
|
||||
tag,
|
||||
// We never want the native sound.
|
||||
silent: true,
|
||||
};
|
||||
|
||||
if (NativeUtils.embedded && NativeUtils.isWindows()) {
|
||||
NativeUtils.flashFrame(true);
|
||||
}
|
||||
|
||||
const notification = new Notification(title, notificationOptions);
|
||||
notification.onclick = () => {
|
||||
if (NativeUtils.embedded) {
|
||||
NativeUtils.focus();
|
||||
} else {
|
||||
window.focus();
|
||||
}
|
||||
options.onClick && options.onClick();
|
||||
};
|
||||
|
||||
if (requireInteraction) {
|
||||
setTimeout(() => notification.close(), 5000);
|
||||
}
|
||||
|
||||
if (!supportsClose) {
|
||||
return {
|
||||
close() {
|
||||
notification.onclose && notification.onclose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
export default {
|
||||
hasPermission,
|
||||
requestPermission,
|
||||
showNotification,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/NotificationUtils.js
|
54
509bba0_unpacked/discord_app/utils/web/ProtocolUtils.js
Executable file
54
509bba0_unpacked/discord_app/utils/web/ProtocolUtils.js
Executable file
|
@ -0,0 +1,54 @@
|
|||
import platform from 'platform';
|
||||
|
||||
function launchFirefox(url, callback) {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
try {
|
||||
iframe.contentWindow.location.href = url;
|
||||
process.nextTick(() => callback(true));
|
||||
} catch (e) {
|
||||
if (e.name === 'NS_ERROR_UNKNOWN_PROTOCOL') {
|
||||
process.nextTick(() => callback(false));
|
||||
}
|
||||
}
|
||||
document.body.removeChild(iframe);
|
||||
}
|
||||
|
||||
function launchChrome(url, callback) {
|
||||
let supported = false;
|
||||
function handleBlur() {
|
||||
supported = true;
|
||||
}
|
||||
|
||||
window.addEventListener('blur', handleBlur);
|
||||
|
||||
location.href = url;
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
callback(supported);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function launchSteam(url, callback) {
|
||||
callback(false);
|
||||
}
|
||||
|
||||
function getLauncher() {
|
||||
if (platform.layout === 'Gecko') {
|
||||
return launchFirefox;
|
||||
} else if (platform.ua.indexOf('Valve Steam GameOverlay') != -1) {
|
||||
return launchSteam;
|
||||
} else {
|
||||
return launchChrome;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
launch: getLauncher(),
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/ProtocolUtils.js
|
43
509bba0_unpacked/discord_app/utils/web/RouterUtils.js
Executable file
43
509bba0_unpacked/discord_app/utils/web/RouterUtils.js
Executable file
|
@ -0,0 +1,43 @@
|
|||
/* @flow */
|
||||
/*
|
||||
* Some notes since react-router changes on the regular:
|
||||
* https://github.com/reactjs/react-router/blob/master/upgrade-guides/v2.0.0.md#history-singletons-provided
|
||||
* https://github.com/reactjs/react-router/blob/master/docs/guides/Histories.md#histories
|
||||
*/
|
||||
import {browserHistory as history} from 'react-router';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Transitions to the URL specified in the arguments by pushing
|
||||
* a new URL onto the history stack.
|
||||
*
|
||||
* @param {String} path
|
||||
*/
|
||||
transitionTo(path: string) {
|
||||
history.push(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Transitions to the URL specified in the arguments by replacing
|
||||
* the current URL in the history stack.
|
||||
*
|
||||
* @param {String} path
|
||||
*/
|
||||
replaceWith(path: string) {
|
||||
history.replace(path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the history object
|
||||
*
|
||||
* @returns {Object} history
|
||||
*/
|
||||
getHistory(): any {
|
||||
return history;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/RouterUtils.js
|
128
509bba0_unpacked/discord_app/utils/web/SoundUtils.js
Executable file
128
509bba0_unpacked/discord_app/utils/web/SoundUtils.js
Executable file
|
@ -0,0 +1,128 @@
|
|||
/* @flow */
|
||||
|
||||
import MediaEngineStore from '../../stores/MediaEngineStore';
|
||||
import NativeUtils from '../NativeUtils';
|
||||
import lodash from 'lodash';
|
||||
|
||||
const DEFEAULT_SINK_ID = 'default';
|
||||
|
||||
let sinkId = DEFEAULT_SINK_ID;
|
||||
|
||||
/**
|
||||
* Determine an output device on WebAudio based on the MediaEngine.
|
||||
* This assumes that devices in both are always listed in the same order.
|
||||
*/
|
||||
function updateSinkId() {
|
||||
window.navigator.mediaDevices
|
||||
.enumerateDevices()
|
||||
.then(devices => {
|
||||
// Find index of selected output device within MediaEngine.
|
||||
const meOutputDevices = MediaEngineStore.getOutputDevices();
|
||||
const meEngineOutputDeviceIndex = lodash(meOutputDevices)
|
||||
.sortBy(device => device.index)
|
||||
.findIndex(device => device.id === MediaEngineStore.getOutputDeviceId());
|
||||
const meEngineOutputDevice = meOutputDevices[MediaEngineStore.getOutputDeviceId()];
|
||||
|
||||
// Find all output devices excluding communications device since it does not exist
|
||||
// in the MediaEngine and select device by index.
|
||||
const outputDevices = devices.filter(
|
||||
device => device.kind === 'audiooutput' && device.deviceId !== 'communications'
|
||||
);
|
||||
let outputDevice = outputDevices[meEngineOutputDeviceIndex];
|
||||
|
||||
// Just incase devices are not in the correct order, attempt to find a device with matching label.
|
||||
if (meEngineOutputDevice != null && (outputDevice == null || outputDevice.label != meEngineOutputDevice.name)) {
|
||||
outputDevice = outputDevices.find(outputDevice => outputDevice.label == meEngineOutputDevice.name);
|
||||
}
|
||||
|
||||
// If somehow the device is not found then just use the default device.
|
||||
sinkId = outputDevice != null ? outputDevice.deviceId : DEFEAULT_SINK_ID;
|
||||
})
|
||||
.catch(() => {
|
||||
sinkId = DEFEAULT_SINK_ID;
|
||||
});
|
||||
}
|
||||
|
||||
if (NativeUtils.embedded) {
|
||||
MediaEngineStore.addChangeListener(updateSinkId);
|
||||
updateSinkId();
|
||||
}
|
||||
|
||||
export class WebAudioSound {
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
_volume: number;
|
||||
_audio: ?Promise;
|
||||
|
||||
constructor(name: string, volume: number) {
|
||||
this.name = name;
|
||||
this._volume = volume;
|
||||
}
|
||||
|
||||
get volume(): number {
|
||||
return this._volume;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
this._volume = value;
|
||||
this._ensureAudio().then(audio => (audio.volume = value));
|
||||
}
|
||||
|
||||
loop() {
|
||||
this._ensureAudio().then(audio => {
|
||||
audio.loop = true;
|
||||
audio.play();
|
||||
});
|
||||
}
|
||||
|
||||
play() {
|
||||
this._ensureAudio().then(audio => {
|
||||
audio.loop = false;
|
||||
audio.play();
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._destroyAudio();
|
||||
}
|
||||
|
||||
_destroyAudio() {
|
||||
// To avoid memory leaks in Chrome the src has to be set to an empty string otherwise even if the JS object
|
||||
// is garbage collected the internal Chrome audio object will live on forever and drain memory and CPU.
|
||||
if (this._audio) {
|
||||
this._audio.then(audio => {
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
});
|
||||
this._audio = null;
|
||||
}
|
||||
}
|
||||
|
||||
_ensureAudio(): Promise {
|
||||
this._audio =
|
||||
this._audio ||
|
||||
new Promise((resolve, reject) => {
|
||||
// $FlowFixMe
|
||||
const audio = new Audio();
|
||||
// $FlowFixMe
|
||||
audio.src = require(`../../sounds/${this.name}.mp3`);
|
||||
audio.onloadeddata = () => {
|
||||
audio.volume = Math.min(MediaEngineStore.getOutputVolume() / 100 * this._volume, 1);
|
||||
if (NativeUtils.embedded) {
|
||||
audio.setSinkId(sinkId);
|
||||
}
|
||||
resolve(audio);
|
||||
};
|
||||
audio.onerror = () => reject();
|
||||
audio.onended = () => this._destroyAudio();
|
||||
audio.load();
|
||||
});
|
||||
return this._audio;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// WEBPACK FOOTER //
|
||||
// ./discord_app/utils/web/SoundUtils.js
|
Loading…
Add table
Add a link
Reference in a new issue