chat-pane
Version:
Solid-compatible Panes: Chat
453 lines (424 loc) • 18.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.longChatPane = void 0;
var _solidLogic = require("solid-logic");
var UI = _interopRequireWildcard(require("solid-ui"));
var $rdf = _interopRequireWildcard(require("rdflib"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/* Long Chat Pane
**
** A long chat consists a of a series of chat files saved by date.
*/
const ns = UI.ns;
const mainClass = ns.meeting('LongChat'); // @@ something from SIOC?
const CHAT_LOCATION_IN_CONTAINER = 'index.ttl#this';
// const menuIcon = 'noun_897914.svg'
const SPANNER_ICON = 'noun_344563.svg';
// resize: horizontal; min-width: 20em;
const SIDEBAR_COMPONENT_STYLE = UI.style.sidebarComponentStyle || ' padding: 0.5em; width: 100%;';
const SIDEBAR_STYLE = UI.style.sidebarStyle || 'overflow-x: auto; overflow-y: auto; border-radius: 1em; border: 0.1em solid white;';
// was purple border
const longChatPane = exports.longChatPane = {
CHAT_LOCATION_IN_CONTAINER,
// noun_704.svg Canoe noun_346319.svg = 1 Chat noun_1689339.svg = three chat
icon: UI.icons.iconBase + 'noun_1689339.svg',
name: 'long chat',
label: function (subject, context) {
const kb = context.session.store;
if (kb.holds(subject, ns.rdf('type'), ns.meeting('LongChat'))) {
// subject is the object
return 'Chat channnel';
}
if (kb.holds(subject, ns.rdf('type'), ns.sioc('Thread'))) {
// subject is the object
return 'Thread';
}
// Looks like a message -- might not havre any class declared
if (kb.any(subject, ns.sioc('content')) && kb.any(subject, ns.dct('created'))) {
return 'message';
}
return null; // Suppress pane otherwise
},
mintClass: mainClass,
mintNew: function (context, newPaneOptions) {
const kb = context.session.store;
const updater = kb.updater;
if (newPaneOptions.me && !newPaneOptions.me.uri) {
throw new Error('chat mintNew: Invalid userid ' + newPaneOptions.me);
}
const newInstance = newPaneOptions.newInstance = newPaneOptions.newInstance || kb.sym(newPaneOptions.newBase + CHAT_LOCATION_IN_CONTAINER);
const newChatDoc = newInstance.doc();
kb.add(newInstance, ns.rdf('type'), ns.meeting('LongChat'), newChatDoc);
kb.add(newInstance, ns.dc('title'), 'Chat channel', newChatDoc);
kb.add(newInstance, ns.dc('created'), new Date(), newChatDoc);
if (newPaneOptions.me) {
kb.add(newInstance, ns.dc('author'), newPaneOptions.me, newChatDoc);
}
const aclBody = (me, resource, AppendWrite) => `
@prefix : <#>.
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix lon: <./${resource}>.
:ControlReadWrite
a acl:Authorization;
acl:accessTo lon:;
acl:agent <${me.uri}>;
acl:default lon:;
acl:mode acl:Control, acl:Read, acl:Write.
:Read
a acl:Authorization;
acl:accessTo lon:;
acl:agentClass foaf:Agent;
acl:default lon:;
acl:mode acl:Read.
:Read${AppendWrite}
a acl:Authorization;
acl:accessTo lon:;
acl:agentClass acl:AuthenticatedAgent;
acl:default lon:;
acl:mode acl:Read, acl:${AppendWrite}.`;
return new Promise(function (resolve, reject) {
updater.put(newChatDoc, kb.statementsMatching(undefined, undefined, undefined, newChatDoc), 'text/turtle', function (uri2, ok, message) {
if (ok) {
resolve(newPaneOptions);
} else {
reject(new Error('FAILED to save new chat channel at: ' + uri2 + ' : ' + message));
}
})
// newChat container authenticated users Append only
.then(result => {
return new Promise((resolve, reject) => {
if (newPaneOptions.me) {
kb.fetcher.webOperation('PUT', newPaneOptions.newBase + '.acl', {
data: aclBody(newPaneOptions.me, '', 'Append'),
contentType: 'text/turtle'
});
kb.fetcher.webOperation('PUT', newPaneOptions.newBase + 'index.ttl.acl', {
data: aclBody(newPaneOptions.me, 'index.ttl', 'Write'),
contentType: 'text/turtle'
});
}
resolve(newPaneOptions);
});
});
});
},
render: function (subject, context, paneOptions) {
const dom = context.dom;
const kb = context.session.store;
/* Preferences
**
** Things like whether to color text by author webid, to expand image URLs inline,
** expanded inline image height. ...
** In general, preferences can be set per user, per user/app combo, per instance,
** and per instance/user combo. Per instance? not sure about unless it is valuable
** for everyone to be seeing the same thing.
*/
// const DCT = $rdf.Namespace('http://purl.org/dc/terms/')
const preferencesFormText = `
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix ui: <http://www.w3.org/ns/ui#>.
@prefix : <#>.
:this
<http://purl.org/dc/elements/1.1/title> "Chat preferences" ;
a ui:Form ;
ui:parts ( :colorizeByAuthor :expandImagesInline :newestFirst :inlineImageHeightEms
:shiftEnterSendsMessage :authorDateOnLeft :showDeletedMessages).
:colorizeByAuthor a ui:TristateField; ui:property solid:colorizeByAuthor;
ui:label "Color user input by user".
:expandImagesInline a ui:TristateField; ui:property solid:expandImagesInline;
ui:label "Expand image URLs inline".
:newestFirst a ui:TristateField; ui:property solid:newestFirst;
ui:label "Newest messages at the top".
:inlineImageHeightEms a ui:IntegerField; ui:property solid:inlineImageHeightEms;
ui:label "Inline image height (lines)".
:shiftEnterSendsMessage a ui:TristateField; ui:property solid:shiftEnterSendsMessage;
ui:label "Shift-Enter sends message".
:authorDateOnLeft a ui:TristateField; ui:property solid:authorDateOnLeft;
ui:label "Author & date of message on left".
:showDeletedMessages a ui:TristateField; ui:property solid:showDeletedMessages;
ui:label "Show placeholders for deleted messages".
`;
const preferencesForm = kb.sym('https://solid.github.io/solid-panes/longCharPane/preferencesForm.ttl#this');
const preferencesFormDoc = preferencesForm.doc();
if (!kb.holds(undefined, undefined, undefined, preferencesFormDoc)) {
// If not loaded already
$rdf.parse(preferencesFormText, kb, preferencesFormDoc.uri, 'text/turtle'); // Load form directly
}
const preferenceProperties = kb.statementsMatching(null, ns.ui.property, null, preferencesFormDoc).map(st => st.object);
// Preferences Menu
//
// Build a menu a the side (@@ reactive: on top?)
async function renderPreferencesSidebar(context) {
// const noun = 'chat room'
const {
dom,
noun
} = context;
const preferencesArea = dom.createElement('div');
preferencesArea.appendChild(panelCloseButton(preferencesArea));
// @@ style below fix .. just make it onviious while testing
preferencesArea.style = SIDEBAR_COMPONENT_STYLE;
preferencesArea.style.minWidth = '25em'; // bit bigger
preferencesArea.style.maxHeight = triptychHeight;
const menuTable = preferencesArea.appendChild(dom.createElement('table'));
const registrationArea = menuTable.appendChild(dom.createElement('tr'));
const statusArea = menuTable.appendChild(dom.createElement('tr'));
const me = _solidLogic.authn.currentUser();
if (me) {
await UI.login.registrationControl({
noun,
me,
statusArea,
dom,
div: registrationArea
}, chatChannel, mainClass);
// eslint-disable-next-line no-console
console.log('Registration control finsished.');
preferencesArea.appendChild(UI.preferences.renderPreferencesForm(chatChannel, mainClass, preferencesForm, {
noun,
me,
statusArea,
div: preferencesArea,
dom,
kb
}));
}
return preferencesArea;
}
// @@ Split out into solid-ui
function panelCloseButton(panel) {
function removePanel() {
panel.parentNode.removeChild(panel);
}
const button = UI.widgets.button(context.dom, UI.icons.iconBase + 'noun_1180156.svg', 'close', removePanel);
button.style.float = 'right';
button.style.margin = '0.7em';
delete button.style.backgroundColor; // do not want white
return button;
}
async function preferencesButtonPressed(_event) {
if (!preferencesArea) {
// Expand
preferencesArea = await renderPreferencesSidebar({
dom,
noun: 'chat room'
});
}
if (paneRight.contains(preferencesArea)) {
// Close menu (hide or delete??)
preferencesArea.parentNode.removeChild(preferencesArea);
preferencesArea = null;
} else {
paneRight.appendChild(preferencesArea);
}
} // preferencesButtonPressed
// All my chats
//
/* Build a other chats list drawer the side
*/
function renderCreationControl(refreshTarget, noun) {
const creationDiv = dom.createElement('div');
const me = _solidLogic.authn.currentUser();
const creationContext = {
// folder: subject,
div: creationDiv,
dom,
noun,
statusArea: creationDiv,
me,
refreshTarget
};
const chatPane = context.session.paneRegistry.byName('chat');
const relevantPanes = [chatPane];
UI.create.newThingUI(creationContext, context, relevantPanes); // Have to pass panes down newUI
return creationDiv;
}
async function renderInstances(theClass, noun) {
const instancesDiv = dom.createElement('div');
const context = {
dom,
div: instancesDiv,
noun
};
await UI.login.registrationList(context, {
public: true,
private: true,
type: theClass
});
instancesDiv.appendChild(renderCreationControl(instancesDiv, noun));
return instancesDiv;
}
let otherChatsArea = null;
async function otherChatsHandler(_event) {
if (!otherChatsArea) {
// Lazy build when needed
// Expand
otherChatsArea = dom.createElement('div');
otherChatsArea.style = SIDEBAR_COMPONENT_STYLE;
otherChatsArea.style.maxHeight = triptychHeight;
otherChatsArea.appendChild(panelCloseButton(otherChatsArea));
otherChatsArea.appendChild(await renderInstances(ns.meeting('LongChat'), 'chat'));
}
// Toggle visibility with button clicks
if (paneLeft.contains(otherChatsArea)) {
otherChatsArea.parentNode.removeChild(otherChatsArea);
} else {
paneLeft.appendChild(otherChatsArea);
}
} // otherChatsHandler
// People in the chat
//
/* Build a participants list drawer the side
*/
let participantsArea;
function participantsHandler(_event) {
if (!participantsArea) {
// Expand
participantsArea = dom.createElement('div');
participantsArea.style = SIDEBAR_COMPONENT_STYLE;
participantsArea.style.maxHeight = triptychHeight;
participantsArea.appendChild(panelCloseButton(participantsArea));
// Record my participation and display participants
const me = _solidLogic.authn.currentUser();
if (!me) alert('Should be logeed in for partipants panel');
UI.pad.manageParticipation(dom, participantsArea, chatChannel.doc(), chatChannel, me, {});
}
// Toggle appearance in sidebar with clicks
// Note also it can remove itself using the X button
if (paneLeft.contains(participantsArea)) {
// Close participants (hide or delete??)
participantsArea.parentNode.removeChild(participantsArea);
participantsArea = null;
} else {
paneLeft.appendChild(participantsArea);
}
} // participantsHandler
let chatChannel = subject;
let selectedMessage = null;
let thread = null;
if (kb.holds(subject, ns.rdf('type'), ns.meeting('LongChat'))) {
// subject is the chatChannel
// eslint-disable-next-line no-console
console.log('@@@ Chat channnel');
// Looks like a message -- might not havre any class declared
} else if (kb.holds(subject, ns.rdf('type'), ns.sioc('Thread'))) {
// subject is the chatChannel
// eslint-disable-next-line no-console
console.log('Thread is subject ' + subject.uri);
thread = subject;
const rootMessage = kb.the(null, ns.sioc('has_reply'), thread, thread.doc());
if (!rootMessage) throw new Error('Thread has no root message ' + thread);
chatChannel = kb.any(null, ns.wf('message'), rootMessage);
if (!chatChannel) throw new Error('Thread root has no link to chatChannel');
} else if (
// Looks like a message -- might not havre any class declared
kb.any(subject, ns.sioc('content')) && kb.any(subject, ns.dct('created'))) {
// eslint-disable-next-line no-console
console.log('message is subject ' + subject.uri);
selectedMessage = subject;
chatChannel = kb.any(null, ns.wf('message'), selectedMessage);
if (!chatChannel) throw new Error('Message has no link to chatChannel');
}
const div = dom.createElement('div');
// Three large columns for particpant, chat, Preferences. formula below just as a note
// const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const triptychHeight = '20cm'; // @@ need to be able to set to window!
const triptych = div.appendChild(dom.createElement('table'));
triptych.style.maxHeight = '12"'; // Screen max
const paneRow = triptych.appendChild(dom.createElement('tr'));
const paneLeft = paneRow.appendChild(dom.createElement('td'));
const paneMiddle = paneRow.appendChild(dom.createElement('td'));
const paneThread = paneRow.appendChild(dom.createElement('td'));
const paneRight = paneRow.appendChild(dom.createElement('td'));
const paneBottom = triptych.appendChild(dom.createElement('tr'));
paneLeft.style = SIDEBAR_STYLE;
paneLeft.style.paddingRight = '1em';
paneThread.style = SIDEBAR_STYLE;
paneThread.style.paddingLeft = '1em';
paneRight.style = SIDEBAR_STYLE;
paneRight.style.paddingLeft = '1em';
paneBottom.appendChild(dom.createElement('td'));
const buttonCell = paneBottom.appendChild(dom.createElement('td'));
paneBottom.appendChild(dom.createElement('td'));
// Button to bring up participants drawer on left
const participantsIcon = 'noun_339237.svg';
const participantsButton = UI.widgets.button(dom, UI.icons.iconBase + participantsIcon, 'participants ...'); // wider var
buttonCell.appendChild(participantsButton);
participantsButton.addEventListener('click', participantsHandler);
// Button to bring up otherChats drawer on left
const otherChatsIcon = 'noun_1689339.svg'; // long chat icon -- not ideal for a set of chats @@
const otherChatsButton = UI.widgets.button(dom, UI.icons.iconBase + otherChatsIcon, 'List of other chats ...'); // wider var
buttonCell.appendChild(otherChatsButton);
otherChatsButton.addEventListener('click', otherChatsHandler);
let preferencesArea = null;
const menuButton = UI.widgets.button(dom, UI.icons.iconBase + SPANNER_ICON, 'Setting ...'); // wider var
buttonCell.appendChild(menuButton);
menuButton.style.float = 'right';
menuButton.addEventListener('click', preferencesButtonPressed);
div.setAttribute('class', 'chatPane');
const options = {
infinite: true
};
const participantsHandlerContext = {
noun: 'chat room',
div,
dom
};
participantsHandlerContext.me = _solidLogic.authn.currentUser(); // If already logged on
async function showThread(thread, options) {
// eslint-disable-next-line no-console
console.log('@@@@ showThread thread ' + thread);
const newOptions = {}; // @@@ inherit
newOptions.thread = thread;
newOptions.includeRemoveButton = true;
newOptions.authorDateOnLeft = options.authorDateOnLeft;
newOptions.newestFirst = options.newestFirst;
paneThread.innerHTML = '';
// eslint-disable-next-line no-console
console.log('Options for showThread message Area', newOptions);
const chatControl = await UI.infiniteMessageArea(dom, kb, chatChannel, newOptions);
chatControl.style.resize = 'both';
chatControl.style.overflow = 'auto';
chatControl.style.maxHeight = triptychHeight;
paneThread.appendChild(chatControl);
}
async function buildPane() {
let prefMap;
try {
prefMap = await UI.preferences.getPreferencesForClass(chatChannel, mainClass, preferenceProperties, participantsHandlerContext);
} catch (err) {
UI.widgets.complain(participantsHandlerContext, err);
}
for (const propuri in prefMap) {
options[propuri.split('#')[1]] = prefMap[propuri];
}
if (selectedMessage) {
options.selectedMessage = selectedMessage;
}
if (paneOptions.solo) {
// This is the top pane, title, scrollbar etc are ours
options.solo = true;
}
if (thread) {
// Rendereing a thread as first class object
options.thread = thread;
} else {
// either show thread *or* allow new threads. Threads don't nest but they could
options.showThread = showThread;
}
const chatControl = await UI.infiniteMessageArea(dom, kb, chatChannel, options);
chatControl.style.resize = 'both';
chatControl.style.overflow = 'auto';
chatControl.style.maxHeight = triptychHeight;
paneMiddle.appendChild(chatControl);
}
// eslint-disable-next-line no-console
buildPane().then(console.log('async - chat pane built'));
return div;
}
};
//# sourceMappingURL=longChatPane.js.map