UNPKG

letsjam

Version:
1,878 lines (1,656 loc) 160 kB
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