Add files

This commit is contained in:
DoomRye 2022-07-26 10:06:20 -07:00
commit bb80829159
18195 changed files with 2122994 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,10 @@
/* @flow */
export function extractId(fingerprint: string) {
return fingerprint.split('.')[0];
}
// WEBPACK FOOTER //
// ./discord_app/utils/FingerprintUtils.js

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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