letsjam
Version:
Ship products faster by adding jam to any project!
1,878 lines (1,656 loc) • 160 kB
JavaScript
import React, { useRef, useEffect, useMemo, useState, useLayoutEffect, createContext, useContext, cloneElement, Component } from 'react';
import { css, keyframes } from 'emotion';
import Mousetrap from 'mousetrap';
import { Base64 } from 'js-base64';
import moment from 'moment';
import { convertFromRaw, EditorState, convertToRaw, getDefaultKeyBinding, RichUtils, KeyBindingUtil, ContentState } from 'draft-js';
import ReactTooltip from 'react-tooltip';
import Editor from 'draft-js-plugins-editor';
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin';
import createLinkifyPlugin from 'draft-js-linkify-plugin';
import createToolbarPlugin from 'draft-js-static-toolbar-plugin';
import { Picker } from 'emoji-mart';
import { useTooltip, TooltipPopup } from '@reach/tooltip';
import { useTransition, config, animated } from 'react-spring';
import DeviceDetector from 'device-detector-js';
import io from 'socket.io-client';
import { select } from 'optimal-select';
import ChipInput from 'material-ui-chip-input';
import MutationObserver from 'react-mutation-observer';
let _ = t => t,
_t,
_t2,
_t3;
function JamButton(props) {
return React.createElement("div", {
onClick: props.toggleJamMode,
className: css(_t || (_t = _`
width: 60px;
height: 60px;
border-radius: 50px;
position: fixed;
bottom: 2rem;
right: 2rem;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
transition: all 0.4s ease-in-out;
background: #f8f9fb;
cursor: pointer;
font-size: 14px;
z-index: 1000000;
${0}
`), props.jamTime && css(_t2 || (_t2 = _`
background: linear-gradient(
0deg,
rgba(115, 229, 191, 0.2),
rgba(115, 229, 191, 0.2)
),
#ffffff;
`)))
}, React.createElement("img", {
src: "https://strawberryjam.nyc3.cdn.digitaloceanspaces.com/icon.png",
className: css(_t3 || (_t3 = _`
width: 30px;
height: 30px;
`))
}));
}
function useKeyBinding(keys, callback) {
let callbackRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
Mousetrap.bind(keys, () => {
if (callbackRef.current) {
callbackRef.current();
}
});
return () => {
Mousetrap.unbind(keys);
};
}, [keys, callbackRef]);
}
function KeyboardShortcuts(props) {
useKeyBinding("j a m", () => {
props.onToggleJamMode();
});
useKeyBinding("p a m", () => {
console.log("missing the office");
});
return null;
}
function SuperShortcuts(props) {
useKeyBinding("shift+u", () => {
props.onToggleUpdates();
});
useKeyBinding("shift+right", () => {
let nextThread = props.currentOpenThread + 1;
if (nextThread === props.threads.length) {
nextThread = 0;
}
props.onChangeOpenThread(nextThread);
if (props.threads[nextThread] !== undefined && props.threads[nextThread].y !== undefined) {
window.scrollTo(0, props.threads[nextThread].y - 100);
}
});
useKeyBinding("shift+left", () => {
let prevThread = props.currentOpenThread - 1;
if (prevThread < 0) {
prevThread = props.threads.length - 1;
}
props.onChangeOpenThread(prevThread);
if (props.threads[prevThread] !== undefined && props.threads[prevThread].y !== undefined) {
window.scrollTo(0, props.threads[prevThread].y - 100);
}
});
useKeyBinding("command+enter", () => {
props.onMessageSend();
});
return null;
}
const API_HOSTNAME = "https://app.jam.dev";
const SOCKET_SERVER = "https://socket-server.jam.dev";
var support = {
searchParams: 'URLSearchParams' in self,
iterable: 'Symbol' in self && 'iterator' in Symbol,
blob:
'FileReader' in self &&
'Blob' in self &&
(function() {
try {
new Blob();
return true
} catch (e) {
return false
}
})(),
formData: 'FormData' in self,
arrayBuffer: 'ArrayBuffer' in self
};
function isDataView(obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}
if (support.arrayBuffer) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
];
var isArrayBufferView =
ArrayBuffer.isView ||
function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
};
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name);
}
if (/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(name)) {
throw new TypeError('Invalid character in header field name')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value);
}
return value
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift();
return {done: value === undefined, value: value}
}
};
if (support.iterable) {
iterator[Symbol.iterator] = function() {
return iterator
};
}
return iterator
}
function Headers(headers) {
this.map = {};
if (headers instanceof Headers) {
headers.forEach(function(value, name) {
this.append(name, value);
}, this);
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
this.append(header[0], header[1]);
}, this);
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name]);
}, this);
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name);
value = normalizeValue(value);
var oldValue = this.map[name];
this.map[name] = oldValue ? oldValue + ', ' + value : value;
};
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)];
};
Headers.prototype.get = function(name) {
name = normalizeName(name);
return this.has(name) ? this.map[name] : null
};
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
};
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = normalizeValue(value);
};
Headers.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this);
}
}
};
Headers.prototype.keys = function() {
var items = [];
this.forEach(function(value, name) {
items.push(name);
});
return iteratorFor(items)
};
Headers.prototype.values = function() {
var items = [];
this.forEach(function(value) {
items.push(value);
});
return iteratorFor(items)
};
Headers.prototype.entries = function() {
var items = [];
this.forEach(function(value, name) {
items.push([name, value]);
});
return iteratorFor(items)
};
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true;
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function() {
reject(reader.error);
};
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise
}
function readBlobAsText(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsText(blob);
return promise
}
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf);
var chars = new Array(view.length);
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i]);
}
return chars.join('')
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0)
} else {
var view = new Uint8Array(buf.byteLength);
view.set(new Uint8Array(buf));
return view.buffer
}
}
function Body() {
this.bodyUsed = false;
this._initBody = function(body) {
this._bodyInit = body;
if (!body) {
this._bodyText = '';
} else if (typeof body === 'string') {
this._bodyText = body;
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body;
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body;
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString();
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer);
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body);
} else {
this._bodyText = body = Object.prototype.toString.call(body);
}
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8');
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type);
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
}
}
};
if (support.blob) {
this.blob = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
};
this.arrayBuffer = function() {
if (this._bodyArrayBuffer) {
return consumed(this) || Promise.resolve(this._bodyArrayBuffer)
} else {
return this.blob().then(readBlobAsArrayBuffer)
}
};
}
this.text = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
};
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
};
}
this.json = function() {
return this.text().then(JSON.parse)
};
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'];
function normalizeMethod(method) {
var upcased = method.toUpperCase();
return methods.indexOf(upcased) > -1 ? upcased : method
}
function Request(input, options) {
options = options || {};
var body = options.body;
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read')
}
this.url = input.url;
this.credentials = input.credentials;
if (!options.headers) {
this.headers = new Headers(input.headers);
}
this.method = input.method;
this.mode = input.mode;
this.signal = input.signal;
if (!body && input._bodyInit != null) {
body = input._bodyInit;
input.bodyUsed = true;
}
} else {
this.url = String(input);
}
this.credentials = options.credentials || this.credentials || 'same-origin';
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers);
}
this.method = normalizeMethod(options.method || this.method || 'GET');
this.mode = options.mode || this.mode || null;
this.signal = options.signal || this.signal;
this.referrer = null;
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(body);
}
Request.prototype.clone = function() {
return new Request(this, {body: this._bodyInit})
};
function decode(body) {
var form = new FormData();
body
.trim()
.split('&')
.forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=');
var name = split.shift().replace(/\+/g, ' ');
var value = split.join('=').replace(/\+/g, ' ');
form.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return form
}
function parseHeaders(rawHeaders) {
var headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
preProcessedHeaders.split(/\r?\n/).forEach(function(line) {
var parts = line.split(':');
var key = parts.shift().trim();
if (key) {
var value = parts.join(':').trim();
headers.append(key, value);
}
});
return headers
}
Body.call(Request.prototype);
function Response(bodyInit, options) {
if (!options) {
options = {};
}
this.type = 'default';
this.status = options.status === undefined ? 200 : options.status;
this.ok = this.status >= 200 && this.status < 300;
this.statusText = 'statusText' in options ? options.statusText : 'OK';
this.headers = new Headers(options.headers);
this.url = options.url || '';
this._initBody(bodyInit);
}
Body.call(Response.prototype);
Response.prototype.clone = function() {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
};
Response.error = function() {
var response = new Response(null, {status: 0, statusText: ''});
response.type = 'error';
return response
};
var redirectStatuses = [301, 302, 303, 307, 308];
Response.redirect = function(url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError('Invalid status code')
}
return new Response(null, {status: status, headers: {location: url}})
};
var DOMException = self.DOMException;
try {
new DOMException();
} catch (err) {
DOMException = function(message, name) {
this.message = message;
this.name = name;
var error = Error(message);
this.stack = error.stack;
};
DOMException.prototype = Object.create(Error.prototype);
DOMException.prototype.constructor = DOMException;
}
function fetch$1(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init);
if (request.signal && request.signal.aborted) {
return reject(new DOMException('Aborted', 'AbortError'))
}
var xhr = new XMLHttpRequest();
function abortXhr() {
xhr.abort();
}
xhr.onload = function() {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
};
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
var body = 'response' in xhr ? xhr.response : xhr.responseText;
resolve(new Response(body, options));
};
xhr.onerror = function() {
reject(new TypeError('Network request failed'));
};
xhr.ontimeout = function() {
reject(new TypeError('Network request failed'));
};
xhr.onabort = function() {
reject(new DOMException('Aborted', 'AbortError'));
};
xhr.open(request.method, request.url, true);
if (request.credentials === 'include') {
xhr.withCredentials = true;
} else if (request.credentials === 'omit') {
xhr.withCredentials = false;
}
if ('responseType' in xhr && support.blob) {
xhr.responseType = 'blob';
}
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value);
});
if (request.signal) {
request.signal.addEventListener('abort', abortXhr);
xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener('abort', abortXhr);
}
};
}
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
})
}
fetch$1.polyfill = true;
if (!self.fetch) {
self.fetch = fetch$1;
self.Headers = Headers;
self.Request = Request;
self.Response = Response;
}
// the whatwg-fetch polyfill installs the fetch() function
// on the global object (window or self)
//
// Return that as the export for use in Webpack, Browserify etc.
var fetchNpmBrowserify = self.fetch.bind(self);
async function jamIsPublic(JAM_ID) {
try {
const res = await fetchNpmBrowserify(`${API_HOSTNAME}/api/plugin/jam/${JAM_ID}/is_public`, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
const json = await res.json();
return json.public;
} catch (e) {
return false;
}
}
async function isAuthenticated(JAM_ID) {
const isPublic = await jamIsPublic(JAM_ID);
if (isPublic) {
const user = window.localStorage.getItem("jamUser");
if (user) return true;
return false;
} else {
if (document.cookie.split(";").some(cookie => cookie.trim().startsWith("jam_filled_cookie="))) {
return true;
} else {
return false;
}
}
}
function getJamFilledCookie() {
try {
const v = document.cookie.match("(^|;) ?" + "jam_filled_cookie" + "=([^;]*)(;|$)");
let cookie = v ? v[0] : null;
if (!cookie) throw new Error("no cookie");
cookie = cookie.split(", domain=")[0];
if (cookie.includes("; ")) return cookie.replace("; ", "");
return cookie;
} catch (e) {
return "jam_filled_cookie=false";
}
}
function getUserIdFromJamFilledCookie() {
try {
const cookie = getJamFilledCookie();
const userId = Base64.decode(cookie.split(".")[1]).replace('{"_id":', "").split('"')[1];
return userId;
} catch (e) {
return false;
}
}
function getUserIdFromLocalStorage() {
return JSON.parse(window.localStorage.getItem("jamUser"))._id;
}
async function isPermissioned(JAM_ID) {
const res = await fetchNpmBrowserify(`${API_HOSTNAME}/api/plugin/jam/${JAM_ID}/check_user_allowed`, {
method: "POST",
headers: {
"content-type": "application/json",
"jam-filled-cookie": getJamFilledCookie(),
"jam-filled-userid": getUserIdFromLocalStorage()
},
body: JSON.stringify({
userId: getUserIdFromJamFilledCookie()
})
});
const json = await res.json();
return json.allowed;
}
async function getUserByIdInJamFilledStorage(JAM_ID) {
try {
const res = await fetchNpmBrowserify(`${API_HOSTNAME}/api/user/${getUserIdFromJamFilledCookie()}`, {
headers: {
"Content-Type": "application/json",
"jam-filled-cookie": getJamFilledCookie(),
"jam-filled-userid": getUserIdFromLocalStorage(),
jamid: JAM_ID
}
});
const json = await res.json();
localStorage.setItem("jamUser", JSON.stringify(json.user));
return true;
} catch (e) {
console.log(e);
return false;
}
}
function getUserFromLocalStorage() {
if (localStorage.getItem("jamUser") && localStorage.getItem("jamUser") !== "undefined") {
return JSON.parse(localStorage.getItem("jamUser") || "");
} else return false;
}
async function getThreads(JAM_ID) {
try {
const res = await fetchNpmBrowserify(`${API_HOSTNAME}/api/plugin/jam/${JAM_ID}/threads`, {
headers: {
"Content-Type": "application/json",
"jam-filled-cookie": getJamFilledCookie(),
"jam-filled-userid": getUserIdFromLocalStorage()
}
});
const json = await res.json();
return json;
} catch (e) {
return false;
}
}
function humanReadableTimeAgo(objectId) {
const commentDate = new Date(parseInt(objectId.substring(0, 8), 16) * 1000);
let string = moment(commentDate).fromNow();
if (string === "a few seconds ago") string = "just now";
string = string.replace("minutes", "min");
return string;
}
function idOrSection(str) {
if (str.includes("-") && str.includes("_")) {
return {
type: "id"
};
}
if (str.includes("@")) {
return {
type: "id"
};
}
if (str.length === 1) {
if (str.match(/[a-z]/i)) {
return {
type: "section"
};
} else {
return {
type: "id"
};
}
}
if (!isNaN(Number(str))) {
return {
type: "id"
};
}
if ((str.match(/[A-Z]/g) || []).length > 2 && (str.match(/[a-z]/g) || []).length > 2) {
return {
type: "id"
};
}
if ((str.match(/-/g) || []).length > 3 || (str.match(/_/g) || []).length > 3) {
return {
type: "id"
};
}
let arr = str.includes("-") ? str.split("-") : str.split("_");
const isIdArray = arr.map(substring => {
if ((substring.match(/[0-9]/g) || []).length > 0 && (substring.match(/[A-Za-z]/g) || []).length > 0) {
return true;
}
if ((substring.match(/[A-Z]/g) || []).length > 2 && (substring.match(/[a-z]/g) || []).length > 2) {
return true;
}
if (!isNaN(Number(substring))) {
return true;
}
if (substring === substring.toUpperCase()) {
return true;
}
return false;
});
if (isIdArray.indexOf(true) > -1) return {
type: "id"
};
return {
type: "section"
};
}
function isNotUndefined(value) {
return typeof value !== "undefined";
}
function createUrlPattern(host, pathname) {
const path = pathname.split("/");
const pathPatternArray = path.map(subpath => {
if (subpath.length === 0) return;
const {
type
} = idOrSection(subpath);
if (type === "section") return subpath;
return "*";
});
const filtered = pathPatternArray.filter(isNotUndefined);
return {
host,
pathPatternArray: filtered
};
}
function checkUrlAgainstPattern(urlObj, urlPatternObj) {
if (urlObj.host !== urlPatternObj.host) {
return false;
}
if (urlObj.pathPatternArray.length !== urlPatternObj.pathPatternArray.length) {
return false;
}
const doItemsMatch = urlPatternObj.pathPatternArray.map((subpath, index) => {
if (subpath === "*") return true;else if (subpath === urlObj.pathPatternArray[index]) return true;else return false;
});
if (doItemsMatch.indexOf(false) > -1) return false;else return true;
}
function positionCommentX(thread, type) {
const {
host,
pathPatternArray
} = createUrlPattern(window.location.host, window.location.pathname);
const correctPageToDisplayThreadOn = checkUrlAgainstPattern({
host,
pathPatternArray
}, {
host: thread.location.host,
pathPatternArray: JSON.parse(thread.location.path)
});
const element = document.querySelector(thread.location.selector);
if (!element || !correctPageToDisplayThreadOn) {
return -1000;
} else {
const rect = element.getBoundingClientRect();
let newX = rect.x + thread.location.percentFromLeft * rect.width;
let coords;
if (type == "preview") {
coords = document.querySelector(`#jam--preview-thread-${thread._id}`).getBoundingClientRect();
} else if (type == "thread") {
coords = document.querySelector(`#jam--thread-container-${thread._id}`) ? document.querySelector(`#jam--thread-container-${thread._id}`).getBoundingClientRect() : document.querySelector(`#jam--preview-thread-${thread._id}`).getBoundingClientRect();
} else {
throw new Error("TODO");
}
if (window.innerWidth < thread.x + coords.width) {
const amountToMoveLeft = thread.x + coords.width - window.innerWidth;
newX = thread.x - amountToMoveLeft - 5;
}
if (newX < 5 || thread.x < 5) {
newX = 5;
}
if (newX > coords.width / 2) {
newX = newX - coords.width / 2;
}
return newX;
}
}
function positionCommentY(thread, type) {
const {
host,
pathPatternArray
} = createUrlPattern(window.location.host, window.location.pathname);
const correctPageToDisplayThreadOn = checkUrlAgainstPattern({
host,
pathPatternArray
}, {
host: thread.location.host,
pathPatternArray: JSON.parse(thread.location.path)
});
const element = document.querySelector(thread.location.selector);
if (!element || !correctPageToDisplayThreadOn) {
return -1000;
} else {
const rect = element.getBoundingClientRect();
let newY = rect.y + window.scrollY + thread.location.percentFromTop * rect.height;
let coords;
if (type == "preview") {
coords = document.querySelector(`#jam--preview-thread-${thread._id}`).getBoundingClientRect();
} else if (type == "thread") {
coords = document.querySelector(`#jam--thread-container-${thread._id}`) ? document.querySelector(`#jam--thread-container-${thread._id}`).getBoundingClientRect() : document.querySelector(`#jam--preview-thread-${thread._id}`).getBoundingClientRect();
} else {
throw new Error("TODO");
}
const body = document.body;
const html = document.documentElement;
const height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
if (height < thread.y + coords.height) {
const amountToMoveUp = thread.y + coords.height - height;
newY = thread.y - amountToMoveUp - 5;
}
if (newY < 5 || thread.y < 5) {
newY = 5;
}
return newY;
}
}
function generateReactionTooltip(emoji, upvotedUsers) {
if (upvotedUsers.length === 0) {
return "";
} else if (upvotedUsers.length === 1) {
return `${upvotedUsers[0]} said ${emoji}`;
} else if (upvotedUsers.length === 2) {
return `${upvotedUsers[0]} and ${upvotedUsers[1]} said ${emoji}`;
} else if (upvotedUsers.length === 3) {
const tempArray = upvotedUsers.slice(0, upvotedUsers.length - 1);
return `${tempArray.join(", ")} and ${upvotedUsers[upvotedUsers.length - 1]} said ${emoji}`;
} else {
const tempArray = upvotedUsers.slice(0, 3);
return `${tempArray.join(", ")} and others said ${emoji}`;
}
}
function JamPreview(props) {
let {
thread
} = props;
let {
comments
} = thread;
let lastComment = comments[comments.length - 1];
let messagePreview = useMemo(() => {
let jsonifiedCommentMessage = JSON.parse(lastComment.message);
let messageConvertedFromRaw = convertFromRaw(jsonifiedCommentMessage);
let tempEditorState = EditorState.createWithContent(messageConvertedFromRaw);
let commentText = tempEditorState.getCurrentContent().getPlainText("\u0001");
return commentText.replace("\u0001", " ");
}, [lastComment]);
let authors = useMemo(() => {
let authors = new Map();
for (let comment of comments) {
if (!authors.has(comment.author.name)) {
authors.set(comment.author.name, comment.author);
}
if (authors.size >= 6) {
break;
}
}
return Array.from(authors.values());
}, [comments]);
let reactions = useMemo(() => {
let reactions = [];
for (let comment of comments.slice(0, 3)) {
if (comment.reactions) {
for (let reaction of comment.reactions) {
reactions.push({
emoji: reaction.emoji,
upvotedUsers: reaction.upvotedUsers
});
}
}
}
return reactions;
}, [comments]);
let [positionedX, setPositionedX] = useState(0);
let [positionedY, setPositionedY] = useState(0);
useLayoutEffect(() => {
setPositionedX(positionCommentX(thread, "thread"));
setPositionedY(positionCommentY(thread, "thread"));
}, [thread]);
return React.createElement("div", {
onClick: props.onClick,
key: `jam--preview-${thread._id}`,
style: {
top: `${positionedY}px`,
left: `${positionedX}px`
},
id: `jam--preview-thread-${thread._id}`,
className: `jam--preview ${thread._id}`
}, React.createElement("div", {
className: "jam--preview-conainer-outer"
}, React.createElement("div", {
className: "jam--preview-avatars"
}, authors.map(author => {
return React.createElement("img", {
key: author._id,
src: author.avatar
});
})), React.createElement("div", {
className: "jam--preview-container"
}, React.createElement("div", {
className: "jam--preview-container-row"
}, React.createElement("p", {
className: "jam--preview-text"
}, messagePreview), React.createElement("div", {
className: "jam--preview-reply-date"
}, humanReadableTimeAgo(lastComment._id))), React.createElement("div", {
className: "jam--preview-container-row"
}, reactions.length > 0 && React.createElement("div", {
className: "jam--preview-reactions"
}, reactions.map((reaction, index) => {
const tooltipText = generateReactionTooltip(reaction.emoji, reaction.upvotedUsers);
return React.createElement("div", {
key: `emoji-${index}`,
className: "jam--preview-reaction",
"data-tip": tooltipText
}, reaction.emoji);
}))), React.createElement(ReactTooltip, {
place: "top",
type: "dark",
effect: "float"
}))), React.createElement("style", null, `
.jam--preview {
max-width: 400px;
height: 66px;
background: #fff;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
border-radius: 10px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
font-family: "SF Pro Text",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
Helvetica, Arial, sans-serif;
color: #2A3632;
position: absolute;
font-size: 14px;
z-index: 1000000;
padding: 5px 10px;
}
@media screen and (max-width: 450px) {
.jam--preview {
width: 365px;
margin-left: auto;
margin-right: auto;
left: 0 !important;
right: 0;
}
}
.jam--preview-conainer-outer {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
z-index: 1000000;
}
.jam--preview-container-row {
align-items: center;
display: flex;
flex-direction: row;
width: 100%;
font-size: 14px;
overflow: hidden;
z-index: 1000000;
}
.jam--preview-container {
display: flex;
flex-direction: column;
max-width: 310px;
z-index: 1000000;
}
.jam--preview-container:hover {
cursor: pointer;
}
.jam--preview-avatars {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 4px;
}
.jam--preview-avatars img {
border-radius: 50%;
height: 40px;
width: 40px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.05);
}
.jam--preview-avatars img:not(:first-child) {
margin-left: -13px;
}
.jam--preview-num-comments {
font-weight: 400;
color: #142dbd;
font-size: 14px;
margin-right: 4px;
white-space: nowrap;
line-height: 20px;
}
.jam--preview-reply-date {
color: rgba(42, 54, 50, .5);
font-size: 14px;
white-space: nowrap;
line-height: 20px;
font-weight: 400;
}
.jam--preview-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: initial;
margin-right: 4px;
margin-left: 4px;
line-height: 20px;
font-size: 14px;
color: #3C81C7;
background: rgba(100, 175, 250, .15);
}
.jam--preview-notification {
align-self: center;
border-radius: 50%;
background: #e8180a;
height: 8px;
width: 8px;
font-size: 14px;
}
#jam--eye-icon-toggle-preview {
margin-right: 6px;
margin-left: 2px;
opacity: 0.5;
}
.jam--preview-reactions {
display: flex;
flex-direction: row;
align-items: center;
padding: 6px;
margin: 0 4px 0 0;
justify-content: center;
box-sizing: border-box;
margin-top: 5px;
height: 12px;
}
.jam--preview-reaction {
margin-top: 1em;
margin-bottom: 1em;
font-size: 10px;
}
`));
}
async function resolveThreadById(threadId, JAM_ID) {
try {
const res = await fetchNpmBrowserify(`${API_HOSTNAME}/api/plugin/jam/${JAM_ID}/threads/${threadId}/delete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"jam-filled-cookie": getJamFilledCookie(),
"jam-filled-userid": getUserIdFromLocalStorage()
}
});
const json = await res.json();
return json;
} catch (e) {
console.log(e);
}
}
async function addMessageToThread(threadId, message, JAM_ID) {
try {
const res = await fetch(`${API_HOSTNAME}/api/plugin/jam/${JAM_ID}/threads/${threadId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"jam-filled-cookie": getJamFilledCookie(),
"jam-filled-userid": getUserIdFromLocalStorage()
},
body: JSON.stringify({
author: getUserIdFromLocalStorage(),
message
})
});
const json = await res.json();
return json;
} catch (e) {
return false;
}
}
function isALoomVideo(loomUrl) {
const loomRe = /http(()|(s)):\/\/((www.)|())loom.com\/((share)|(embed))\/.[^ ]*/g;
let loomUrlArr = loomRe.exec(loomUrl);
if (loomUrlArr === null) {
return null;
} else {
let embedUrl = loomUrlArr[0];
if (embedUrl.includes("share")) {
embedUrl = embedUrl.replace("share", "embed");
return embedUrl;
}
return embedUrl;
}
}
let {
hasCommandModifier
} = KeyBindingUtil;
function processKeyBinding(event) {
if (event.keyCode === 13 && hasCommandModifier(event)) {
return "jameditor-enter";
}
return getDefaultKeyBinding(event);
}
let mentionsTheme = {
mention: "mention",
mentionSuggestions: "mentionSuggestions",
mentionSuggestionsEntry: "mentionSuggestionsEntry",
mentionSuggestionsEntryFocused: "mentionSuggestionsEntryFocused",
mentionSuggestionsEntryText: "mentionSuggestionsEntryText",
mentionSuggestionsEntryAvatar: "mentionSuggestionsEntryAvatar",
mentionSuggestionsEntryContainer: "mentionSuggestionsEntryContainer",
mentionSuggestionsEntryContainerRight: "mentionSuggestionsEntryContainerRight",
mentionSuggestionsEntryTitle: "mentionSuggestionsEntryTitle"
};
let linkifyPlugin = createLinkifyPlugin();
let staticToolbarPlugin = createToolbarPlugin();
function Entry(props) {
let {
mention,
theme,
...parentProps
} = props;
return React.createElement("div", Object.assign({}, parentProps), React.createElement("div", {
className: theme.mentionSuggestionsEntryContainer
}, React.createElement("div", {
className: theme.mentionSuggestionsEntryContainerLeft
}, React.createElement("img", {
src: mention.avatar,
className: theme.mentionSuggestionsEntryAvatar,
role: "presentation"
})), React.createElement("div", {
className: theme.mentionSuggestionsEntryContainerRight
}, React.createElement("div", {
className: theme.mentionSuggestionsEntryText
}, mention.name), React.createElement("div", {
className: theme.mentionSuggestionsEntryTitle
}, mention.title))));
}
function getEmbeddedVideoSrc(text) {
if (!text.includes("loom")) return null;
if (!(text.includes("share") || text.includes("embed"))) return null;
return isALoomVideo(text);
}
function JamEditor(props) {
let {
editorState,
mentions
} = props;
let editorRef = useRef(null);
let mentionPlugin = useMemo(() => {
return createMentionPlugin({
mentions,
entityMutability: "IMMUTABLE",
theme: mentionsTheme,
mentionPrefix: "@",
supportWhitespace: true
});
}, [mentions]);
let [suggestions, setSuggestions] = useState(props.mentions);
let [readOnly, setReadOnly] = useState(false);
let embedVideoSrc = useMemo(() => {
let state = convertToRaw(editorState.getCurrentContent());
for (let block of state.blocks) {
let text = block.text;
let embedVideoSrc = getEmbeddedVideoSrc(text);
if (embedVideoSrc !== null) return embedVideoSrc;
}
return null;
}, [editorState]);
function handleChange(editorState) {
props.onEditorChange(editorState);
}
function handleSearchChange({
value
}) {
setSuggestions(defaultSuggestionsFilter(value, props.mentions));
}
function handleAddMention() {}
function handleKeyCommand(command, editorState) {
if (command === "jameditor-enter") {
setReadOnly(false);
props.onMessageSend();
return "handled";
}
let newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
handleChange(newState);
return "handled";
}
return "not-handled";
}
let MentionSuggestions = mentionPlugin.MentionSuggestions;
let plugins = [mentionPlugin, linkifyPlugin, staticToolbarPlugin];
return React.createElement("div", {
className: "jam--editor-draftjs-wrapper lds-grid"
}, React.createElement(Editor, {
editorState: props.editorState,
handleKeyCommand: handleKeyCommand,
onChange: handleChange,
plugins: plugins,
ref: ref => {
editorRef.current = ref;
},
placeholder: "Type a comment...",
keyBindingFn: processKeyBinding,
readOnly: readOnly
}), React.createElement(MentionSuggestions, {
onSearchChange: handleSearchChange,
suggestions: suggestions,
onAddMention: handleAddMention,
entryComponent: Entry
}), embedVideoSrc !== null && React.createElement("div", {
className: "jam--embed-video-container"
}, React.createElement("iframe", {
width: "100%",
height: "100%",
src: embedVideoSrc,
frameBorder: "0",
allowFullScreen: true
}), React.createElement("style", null, `
.jam--embed-video-container {
padding: 15px;
}
`)), React.createElement("style", null, `
.jam--suggest-css {
box-sizing: border-box;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
margin-left: 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo,
Courier, monospace;
font-size: 10px;
margin-bottom: 20px;
font-weight: 600;
color: #47b38f;
background: rgba(115, 229, 191, 0.15);
border: none;
}
.jam--editor-draftjs-wrapper {
width: 100%;
padding: 0.5rem 0;
}
.DraftEditor-root {
width: 100%;
box-sizing: border-box;
padding: 0px 1rem 1rem;
padding-top: 0px;
padding-right: 1rem;
padding-bottom: 1rem;
padding-left: 1rem;
font-size: 14px;
line-height: 20px;
font-weight: 400;
color: #2a3632;
}
.DraftEditor-root a {
text-decoration: none;
color: #3875fd;
}
.DraftEditor-editorContainer {
background-color: rgba(255, 255, 255, 0);
border-left: 0.1px solid transparent;
position: relative;
text-align: left;
z-index: 1;
}
.draftJsToolbar__buttonWrapper__1Dmqh {
display: inline-block;
}
.draftJsToolbar__button__qi1gf {
background: #fbfbfb;
color: #888;
font-size: 18px;
border: 0;
padding-top: 5px;
vertical-align: bottom;
height: 34px;
width: 36px;
}
.draftJsToolbar__button__qi1gf svg {
fill: #888;
}
.draftJsToolbar__button__qi1gf:hover,
.draftJsToolbar__button__qi1gf:focus {
background: #f3f3f3;
outline: 0; /* reset for :focus */
}
.draftJsToolbar__active__3qcpF {
background: #efefef;
color: #444;
}
.draftJsToolbar__active__3qcpF svg {
fill: #444;
}
.draftJsToolbar__separator__3U7qt {
display: inline-block;
border-right: 1px solid #ddd;
height: 24px;
margin: 0 0.5em;
}
.draftJsToolbar__toolbar__dNtBH {
border: none;
padding: 0.25rem;
background: #fff;
border-radius: 2px;
box-shadow: none;
z-index: 2;
box-sizing: border-box;
}
.draftJsToolbar__toolbar__dNtBH:after {
border-color: rgba(255, 255, 255, 0);
border-top-color: #fff;
border-width: 4px;
margin-left: -4px;
}
.draftJsToolbar__toolbar__dNtBH:before {
border-color: rgba(221, 221, 221, 0);
border-top-color: #ddd;
border-width: 6px;
margin-left: -6px;
}
.DraftEditor-alignLeft .public-DraftEditorPlaceholder-root {
left: 0;
text-align: left;
}
.DraftEditor-alignCenter .public-DraftEditorPlaceholder-root {
margin: 0 auto;
text-align: center;
width: 100%;
}
.DraftEditor-alignRight .public-DraftEditorPlaceholder-root {
right: 0;
text-align: right;
}
.public-DraftEditorPlaceholder-root {
color: #9197a3;
position: absolute;
z-index: 1;
}
.public-DraftEditorPlaceholder-hasFocus {
color: #bdc1c9;
}
.DraftEditorPlaceholder-hidden {
display: none;
}
.editor {
box-sizing: border-box;
border: 1px solid #ddd;
cursor: text;
padding: 16px;
border-radius: 2px;
margin-bottom: 2em;
box-shadow: inset 0px 1px 8px -3px #ababab;
background: #fefefe;
}
.editor :global(.public-DraftEditor-content) {
min-height: 140px;
}
.draftJsMentionPlugin__mention__29BEd,
.draftJsMentionPlugin__mention__29BEd:visited {
color: #575f67;
cursor: pointer;
display: inline-block;
background: #e6f3ff;
padding-left: 2px;
padding-right: 2px;
border-radius: 2px;
text-decoration: none;
}
.draftJsMentionPlugin__mention__29BEd:hover,
.draftJsMentionPlugin__mention__29BEd:focus {
color: #677584;
background: #edf5fd;
outline: 0;
}
.draftJsMentionPlugin__mention__29BEd:active {
color: #222;
background: #455261;
}
.draftJsMentionPlugin__mentionSuggestionsEntry__3mSwm {
padding: 7px 10px 3px 10px;
}
.draftJsMentionPlugin__mentionSuggestionsEntry__3mSwm:active {
background-color: #cce7ff;
}
.draftJsMentionPlugin__mentionSuggestionsEntryFocused__3LcTd {
background-color: #e6f3ff;
}
.draftJsMentionPlugin__mentionSuggestionsEntryText__3Jobq {
display: inline-block;
margin-left: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 368px;
font-size: 0.9em;
margin-bottom: 0.2em;
}
.draftJsMentionPlugin__mentionSuggestionsEntryAvatar__1xgA9 {
display: inline-block;
width: 24px;
height: 24px;
border-radius: 12px;
}
.draftJsMentionPlugin__mentionSuggestions__2DWjA {
border: 1px solid #eee;
margin-top: 0.4em;
position: absolute;
min-width: 220px;
max-width: 440px;
background: #fff;
border-radius: 2px;
box-shadow: 0px 4px 30px 0px rgba(220, 220, 220, 1);
cursor: pointer;
padding-top: 8px;
padding-bottom: 8px;
z-index: 2;
display: -webkit-box;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
box-sizing: border-box;
-webkit-transform: scale(0);
transform: scale(0);
}
.mention {
color: #3776fd;
text-decoration: none;
}
.mentionSuggestions {
border-top: 1px solid #eee;
background: #fff;
border-radius: 2px;
cursor: pointer;
padding-top: 8px;
padding-bottom: 8px;
display: flex;
flex-direction: column;
box-sizing: border-box;
transform-origin: 50% 0%;
transform: scaleY(0);
margin: -16px;
margin-top: 5px;
}
.mentionSuggestionsEntryContainer {
display: table;
width: 100%;
}
.mentionSuggestionsEntryContainerLeft,
.mentionSuggestionsEntryContainerRight {
display: table-cell;
vertical-align: middle;
}
.mentionSuggestionsEntryContainerRight {
width: 100%;
padding-left: 8px;
}
.mentionSuggestionsEntry {
/* padding: 7px 10px 3px 10px; */
padding: 0;
transition: background-color 0.4s
cubic-bezier(0.27, 1.27, 0.48, 0.56);
color: rgb(55, 60, 77);
}
.mentionSuggestionsEntry:active {
/* background-color: #cce7ff; */
/* font-weight: 500; */
color: #000;
}
.mentionSuggestionsEntryFocused {
composes: mentionSuggestionsEntry;
/* background-color: #e6f3ff; */
font-weight: 500;
}
.mentionSuggestionsEntryText,
.mentionSuggestionsEntryTitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mentionSuggestionsEntryText {
color: rgb(55, 60, 77);
}
.mentionSuggestionsEntryTitle {
font-size: 80%;
color: #a7a7a7;
}
.mentionSuggestionsEntryAvatar {
display: block;
width: 25px;
height: 25px;
border-radius: 50%;
margin-left: 25px;
margin-top: 5px;
margin-bottom: 5px;
}
`));
}
const ConfettiContext = createContext({
celebrate: () => {}
});
function Confetti({
confettiCharacters
}) {
return React.createElement("div", null, React.createElement("div", null, confettiCharacters.map((c, i) => {
return React.createElement("span", {
key: i,
style: {
left: `${Math.random() * (97 - 3 + 1) + 3}%`,
animationDelay: `${Math.random() * (3 - 0 + 1) + 0}s`,
animationDuration: `${Math.random() * (2 - 1 + 1) + 1}s`
},
className: "jam--confetti-piece"
}, c);
})), React.createElement("style", null, `
.jam--confetti-piece {
animation: falling 2s linear 1;
position: fixed;
top: 110%;
left: 50%;
font-size: 5em;
z-index: 1000003;
}
@keyframes falling {
0% {
transform: translate3D(0, -150vh, 0) rotate(-540deg);
}
100% {
transform: translate3D(0, 0, 0) rotate(0deg);
}
}
`));
}
const ConfettiProvider = ({
children,
timeout: _timeout = 3000
}) => {
const [shouldShowConfetti, setShouldShowConfetti] = useState(false);
const confettiCharacters = useRef(["🔥", "🎉", "🌮", "🎊", "🎉", "🔥", "🎉", "🌮", "🎊", "🎉", "🔥", "🎉", "🌮", "🎊", "🎉", "🔥", "🎉", "🌮", "🎊", "🎉", "🔥", "🎉", "🌮", "🎊", "🎉", "🔥", "🎉", "🌮", "🎊", "🎉"]);
useEffect(() => {
if (!shouldShowConfetti) {
return;
}
const confettiCleanupTimer = setTimeout(() => setShouldShowConfetti(false), _timeout);
return () => clearTimeout(confettiCleanupTimer);
}, [shouldShowConfetti, _timeout]);
return React.createElement(ConfettiContext.Provider, {
value: {
celebrate: characters => {
if (characters) {
confettiCharacters.current = characters;
}
setShouldShowConfetti(true);
}
}
}, shouldShowConfetti && React.createElement(Confetti, {
confettiCharacters: confettiCharacters.current
}), children);
};
const UserContext = createContext({
user: {
_id: "",
avatar: "",
commentCount: 0,
name: ""
},
updateUser: () => {}
});
function JamThreadEditor(props) {
let [editorState, setEditorState] = useState(() => EditorState.createEmpty());
const {
celebrate
} = useContext(ConfettiContext);
const {
user: loggedInUser,
updateUser
} = useContext(UserContext);
function handleEditorChange(newEditorState) {
setEditorState(newEditorState);
}
async function handleMessageSend() {
let message = convertToRaw(editorState.getCurrentContent());
props.onLoadingChange(true);
let newThread = await addMessageToThread(props.thread._id, JSON.stringify(message), props.JAM_ID);
if (newThread) {
props.onThreadChange(newThread);
}
props.onLoadingChange(false);
if (loggedInUser && loggedInUser.commentCount === 0) {
celebrate();
}
if (loggedInUser) {
updateUser({
commentCount: loggedInUser.commentCount + 1
});
}
props.socket.emit("threadUpdate", newThread);
setEditorState(EditorState.push(editorState, ContentState.createFromText(""), "remove-range"));
}
return React.createElement("div", {
className: "jam--thread-editor-container"
}, React.createElement("div", {
className: "jam--thread-header-container"
}, loggedInUser && React.createElement("div", {
style: {
display: "flex"
}
}, React.createElement("img", {
className: "jam--thread-header-author-avatar",
src: loggedInUser.avatar
}), React.createElement("div", {
className: "jam--thread-header-author-name"
}, loggedInUser.name)), React.createElement("div", {
onClick: handleMessageSend,
className: "jam--button jam--thread-editor-send-button"
}, props.loading ? "Sending..." : "Send", React.createElement("img", {
className: "jam--tiny-berry",
src: "https://strawberryjam.nyc3.cdn.digitaloceanspaces.com/icon.png"
}))), React.createElement(JamEditor, {
onEditorChange: handleEditorChange,
editorState: editorState,
onMessageSend: handleM