Task lists WIP:
* pubsub manager * some refactoring * various fixes
This commit is contained in:
parent
e8e8af855d
commit
df788473cd
@ -1 +1,2 @@
|
||||
export const XMLNS_TASKLIST = 'https://livingston.frama.io/peertube-plugin-livechat/protocol/tasklist'
|
||||
export const XMLNS_TASKLIST = 'urn:peertube-plugin-livechat:tasklist'
|
||||
export const XMLNS_TASK = 'urn:peertube-plugin-livechat:task'
|
||||
|
@ -8,6 +8,8 @@ import './muc-task-list-view.js' // FIXME: here or in another file?
|
||||
import './muc-task-lists-view.js' // FIXME: here or in another file?
|
||||
import './modals/muc-task-lists.js' // FIXME: here or in another file?
|
||||
|
||||
// TODO: add a client disco feature (using api.listen.on('addClientFeatures' ...)).
|
||||
|
||||
converse.plugins.add('livechat-converse-tasks', {
|
||||
dependencies: ['converse-muc', 'converse-disco'], // TODO: add converse-pubsub
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { Collection } from '@converse/skeletor/src/collection.js'
|
||||
import { ChatRoomTaskList } from './task-list'
|
||||
import { XMLNS_TASKLIST } from './constants'
|
||||
import { initStorage } from '@converse/headless/utils/storage.js'
|
||||
import { getUniqueId } from '@converse/headless/utils/core.js'
|
||||
import { converse, api } from '@converse/headless/core'
|
||||
const { $build } = converse.env
|
||||
|
||||
@ -24,8 +23,6 @@ class ChatRoomTaskLists extends Collection {
|
||||
initStorage(this, id, 'session')
|
||||
|
||||
this.on('change:name', () => this.sort())
|
||||
|
||||
this.fetchTasksLists().catch(console.error)
|
||||
}
|
||||
|
||||
comparator (tasklist1, tasklist2) {
|
||||
@ -35,50 +32,6 @@ class ChatRoomTaskLists extends Collection {
|
||||
return name1 < name2 ? -1 : name1 > name2 ? 1 : 0
|
||||
}
|
||||
|
||||
create (attrs, options) {
|
||||
if (attrs instanceof ChatRoomTaskList) {
|
||||
return super.create(attrs, options)
|
||||
}
|
||||
attrs.id ??= getUniqueId()
|
||||
return super.create(attrs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires Task lists from the server.
|
||||
*/
|
||||
async fetchTasksLists () {
|
||||
// TODO: remove these test lines, and subscribe to pubsub.
|
||||
const taskListsData = [
|
||||
{
|
||||
id: 'task-list-1',
|
||||
name: 'Task List 1'
|
||||
},
|
||||
{
|
||||
id: 'task-list-2',
|
||||
name: 'Task List 2'
|
||||
}
|
||||
]
|
||||
|
||||
for (const item of taskListsData) {
|
||||
let id = item.id
|
||||
|
||||
const tasklist = id ? this.get(id) : undefined
|
||||
if (tasklist) {
|
||||
tasklist.save({
|
||||
name: item.name
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id ??= getUniqueId()
|
||||
|
||||
this.create({
|
||||
id,
|
||||
name: item.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async createTaskList (data) {
|
||||
const name = data?.name
|
||||
if (!name) { throw new Error('Missing name') }
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Collection } from '@converse/skeletor/src/collection.js'
|
||||
import { ChatRoomTask } from './task'
|
||||
import { initStorage } from '@converse/headless/utils/storage.js'
|
||||
import { getUniqueId } from '@converse/headless/utils/core.js'
|
||||
|
||||
/**
|
||||
* A list of {@link _converse.ChatRoomTask} instances, representing all tasks associated to a MUC.
|
||||
@ -22,71 +21,6 @@ class ChatRoomTasks extends Collection {
|
||||
initStorage(this, id, 'session')
|
||||
|
||||
this.on('change:order', () => this.sort())
|
||||
|
||||
this.fetchTasks().catch(console.error)
|
||||
}
|
||||
|
||||
create (attrs, options) {
|
||||
if (attrs instanceof ChatRoomTask) {
|
||||
return super.create(attrs, options)
|
||||
}
|
||||
attrs.id ??= getUniqueId()
|
||||
return super.create(attrs, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires Task lists from the server.
|
||||
*/
|
||||
async fetchTasks () {
|
||||
// TODO: remove these test lines, and subscribe to pubsub.
|
||||
const tasksData = [
|
||||
{
|
||||
id: 'task-1',
|
||||
name: 'Task 1.1',
|
||||
list: 'task-list-1',
|
||||
order: 1,
|
||||
done: false
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
name: 'Task 1.2',
|
||||
list: 'task-list-1',
|
||||
order: 2,
|
||||
done: true
|
||||
},
|
||||
{
|
||||
id: 'task-3',
|
||||
name: 'Task 2.1',
|
||||
list: 'task-list-2',
|
||||
order: 1,
|
||||
done: false
|
||||
}
|
||||
]
|
||||
|
||||
for (const item of tasksData) {
|
||||
let id = item.id
|
||||
|
||||
const task = id ? this.get(id) : undefined
|
||||
if (task) {
|
||||
task.save({
|
||||
name: item.name,
|
||||
list: item.list,
|
||||
order: item.order,
|
||||
done: item.done
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id ??= getUniqueId()
|
||||
|
||||
this.create({
|
||||
id,
|
||||
name: item.name,
|
||||
list: item.list,
|
||||
order: item.order,
|
||||
done: item.done
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { XMLNS_TASKLIST, XMLNS_TASK } from './constants.js'
|
||||
import { PubSubManager } from '../../shared/lib/pubsub-manager.js'
|
||||
import { converse, _converse, api } from '../../../src/headless/core.js'
|
||||
import { __ } from 'i18n'
|
||||
const { Strophe, $iq } = converse.env
|
||||
|
||||
export function getHeadingButtons (view, buttons) {
|
||||
const muc = view.model
|
||||
@ -13,6 +14,9 @@ export function getHeadingButtons (view, buttons) {
|
||||
return buttons
|
||||
}
|
||||
|
||||
// TODO: use disco to discover the feature.
|
||||
// (if the chat is remote, the server could use a livechat version that does not support this feature)
|
||||
|
||||
// Adding a "Open task list" button.
|
||||
buttons.unshift({
|
||||
// eslint-disable-next-line no-undef
|
||||
@ -32,7 +36,7 @@ export function getHeadingButtons (view, buttons) {
|
||||
}
|
||||
|
||||
function _initChatRoomTaskLists (mucModel) {
|
||||
if (mucModel.tasklists) {
|
||||
if (mucModel.taskManager) {
|
||||
// already initiliazed
|
||||
return
|
||||
}
|
||||
@ -40,35 +44,41 @@ function _initChatRoomTaskLists (mucModel) {
|
||||
mucModel.tasklists = new _converse.ChatRoomTaskLists(undefined, { chatroom: mucModel })
|
||||
mucModel.tasks = new _converse.ChatRoomTasks(undefined, { chatroom: mucModel })
|
||||
|
||||
// Requesting all items.
|
||||
const stanza = $iq({
|
||||
type: 'get',
|
||||
from: _converse.bare_jid,
|
||||
to: mucModel.get('jid')
|
||||
}).c('pubsub', { xmlns: Strophe.NS.PUBSUB })
|
||||
.c('items', { node: 'livechat-tasks' })
|
||||
|
||||
api.sendIQ(stanza).then(
|
||||
(iq) => {
|
||||
console.debug('task lists: ', iq)
|
||||
},
|
||||
(iq) => {
|
||||
if (iq === null || !iq?.querySelector) {
|
||||
console.error('Failed to retrieve tasks', iq)
|
||||
return
|
||||
mucModel.taskManager = new PubSubManager(
|
||||
mucModel.get('jid'),
|
||||
'livechat-tasks', // the node name
|
||||
{
|
||||
tasklist: {
|
||||
itemTag: 'tasklist',
|
||||
xmlns: XMLNS_TASKLIST,
|
||||
collection: mucModel.tasklists,
|
||||
fields: {
|
||||
name: String
|
||||
}
|
||||
},
|
||||
task: {
|
||||
itemTag: 'task',
|
||||
xmlns: XMLNS_TASK,
|
||||
collection: mucModel.tasks,
|
||||
fields: {
|
||||
name: String
|
||||
},
|
||||
attributes: {
|
||||
done: Boolean,
|
||||
list: String,
|
||||
order: Number
|
||||
}
|
||||
}
|
||||
if (!iq.querySelector('error[type="cancel"] item-not-found')) {
|
||||
console.error('Failed to retrieve tasks:', iq)
|
||||
return
|
||||
}
|
||||
// This is totally normal when you open an empty task list.
|
||||
console.log('Not livechat-tasks node for now')
|
||||
}
|
||||
)
|
||||
mucModel.taskManager.start().catch(err => console.log(err))
|
||||
}
|
||||
|
||||
function _destroyChatRoomTaskLists (mucModel) {
|
||||
if (!mucModel.tasklists) { return }
|
||||
if (!mucModel.taskManager) { return }
|
||||
|
||||
mucModel.taskManager.stop().catch(err => console.log(err))
|
||||
mucModel.taskManager = undefined
|
||||
|
||||
// mucModel.tasklists.unload() FIXME: add a method to unregister from the pubsub, and empty the tasklist.
|
||||
mucModel.tasklists = undefined
|
||||
|
248
conversejs/custom/shared/lib/pubsub-manager.js
Normal file
248
conversejs/custom/shared/lib/pubsub-manager.js
Normal file
@ -0,0 +1,248 @@
|
||||
import { converse, _converse, api } from '../../../src/headless/core.js'
|
||||
const { Strophe, $iq, sizzle } = converse.env
|
||||
|
||||
/**
|
||||
* This class helps to manage some objects that are stored on pubsub nodes.
|
||||
* This if for example used for livechat-taskslists and livechat-tasks,
|
||||
* but could be used for other object types that would be added one day.
|
||||
*/
|
||||
export class PubSubManager {
|
||||
roomJID
|
||||
node
|
||||
types
|
||||
stanzaHandler
|
||||
|
||||
/**
|
||||
* Created a new pubsub manager.
|
||||
* @param {string} roomJID the room JID
|
||||
* @param {string} node the node name to which subscribe
|
||||
* @param {Array} types an array with object describing the object type, and how to handle them. Here is its format:
|
||||
* {
|
||||
* itemTag: 'task',
|
||||
* xmlns: XMLNS_TASK,
|
||||
* collection: mucModel.tasks,
|
||||
* fields: { // these are item child nodes (tag names)
|
||||
* name: String,
|
||||
* },
|
||||
* attributes: { // these are attribute on the node
|
||||
* order: Number
|
||||
* done: Boolean,
|
||||
* list: String,
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
constructor (roomJID, node, types) {
|
||||
this.roomJID = roomJID
|
||||
this.node = node
|
||||
this.types = types
|
||||
|
||||
this.stanzaHandler = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the manager.
|
||||
* This will subscribe to the node, and retrieve all existing objects.
|
||||
*/
|
||||
async start () {
|
||||
// FIXME: handle errors. Find a way to display to user that this failed.
|
||||
|
||||
this.stanzaHandler = _converse.connection.addHandler(
|
||||
(message) => {
|
||||
try {
|
||||
this._handleMessage(message)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
return true // if returning anything else, the handler will not be called again!
|
||||
},
|
||||
null, // The namespace to match.
|
||||
'message', // The stanza name to match.
|
||||
'headline', // The stanza type attribute to match.
|
||||
null, // The stanza id attribute to match.
|
||||
this.roomJID, // The stanza from attribute to match.
|
||||
{
|
||||
matchBareFromJid: true
|
||||
} // The handler options
|
||||
)
|
||||
await this._subscribe()
|
||||
await this._retrieveAllItems()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the manager
|
||||
*/
|
||||
async stop () {
|
||||
// await this._unsubscribe() TODO
|
||||
|
||||
if (this.stanzaHandler) {
|
||||
_converse.connection.deleteHandler(this.stanzaHandler)
|
||||
this.stanzaHandler = undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribed to the pubsub node.
|
||||
*/
|
||||
async _subscribe () {
|
||||
const stanza = $iq({
|
||||
type: 'set',
|
||||
from: _converse.jid,
|
||||
to: this.roomJID
|
||||
}).c('pubsub', { xmlns: Strophe.NS.PUBSUB })
|
||||
.c('subscribe', { node: this.node, jid: _converse.bare_jid })
|
||||
|
||||
try {
|
||||
const iq = await api.sendIQ(stanza)
|
||||
|
||||
console.debug('subscribtion ok: ', iq)
|
||||
} catch (iq) {
|
||||
console.error('Failed to subscribe to ' + this.node, iq)
|
||||
throw iq
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all items
|
||||
*
|
||||
* TODO: handle pagination if results are not all sent.
|
||||
* See https://xmpp.org/extensions/xep-0060.html#subscriber-retrieve-returnsome
|
||||
*/
|
||||
async _retrieveAllItems () {
|
||||
// Requesting all items.
|
||||
const stanza = $iq({
|
||||
type: 'get',
|
||||
from: _converse.jid,
|
||||
to: this.roomJID
|
||||
}).c('pubsub', { xmlns: Strophe.NS.PUBSUB })
|
||||
.c('items', { node: this.node })
|
||||
|
||||
try {
|
||||
const iq = await api.sendIQ(stanza)
|
||||
this._handleIQ(iq)
|
||||
} catch (iq) {
|
||||
if (iq === null || !iq?.querySelector) {
|
||||
console.error('Failed to retrieve objects from ' + this.node, iq)
|
||||
throw iq
|
||||
}
|
||||
if (!iq.querySelector('error[type="cancel"] item-not-found')) {
|
||||
console.error('Failed to retrieve objects from ' + this.node + ':', iq)
|
||||
throw iq
|
||||
}
|
||||
// This is totally normal when you open an empty node.
|
||||
console.log('Not ' + this.node + ' node for now')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an incomming message contains data to dispatch.
|
||||
* @param message The incoming stanza
|
||||
*/
|
||||
_handleMessage (message) {
|
||||
const itemsNodes = sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"] items[node="${this.node}"]`, message)
|
||||
if (!itemsNodes.length) {
|
||||
return
|
||||
}
|
||||
for (const itemsNode of itemsNodes) {
|
||||
this._dispatchStanza(itemsNode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As _handleMessage, but for IQ.
|
||||
* @param iq stanza
|
||||
*/
|
||||
_handleIQ (iq) {
|
||||
const itemsNodes = sizzle(`pubsub items[node="${this.node}"]`, iq)
|
||||
if (!itemsNodes.length) { return }
|
||||
for (const itemsNode of itemsNodes) {
|
||||
this._dispatchStanza(itemsNode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse items in a stanza response, and dispatch them to collections.
|
||||
* @param {stanza} itemsNode the 'items' part of the incomming stanza.
|
||||
*/
|
||||
_dispatchStanza (itemsNode) {
|
||||
console.debug('Dispatching items for node ' + this.node + ' from stanza items: ', itemsNode)
|
||||
|
||||
for (const key in this.types) {
|
||||
const type = this.types[key]
|
||||
|
||||
const selector = `item ${type.itemTag}[xmlns="${type.xmlns}"]`
|
||||
const items = sizzle(selector, itemsNode)
|
||||
if (!items.length) { continue }
|
||||
|
||||
console.log('Found ' + items.length + ' ' + type.itemTag + ' in stanza, dispatching...')
|
||||
for (const item of items) {
|
||||
// Note: we consider that there is only one object in an item.
|
||||
const id = item.parentNode?.getAttribute('id')
|
||||
if (!id) {
|
||||
console.error('Missing id for this item', item)
|
||||
continue
|
||||
}
|
||||
|
||||
const data = this._parseItem(item, type)
|
||||
if (data === null) { continue }
|
||||
|
||||
const existing = type.collection.get(id)
|
||||
if (existing) {
|
||||
existing.save(data)
|
||||
} else {
|
||||
type.collection.create(Object.assign(data, { id }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._handleRetractations(itemsNode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse retractations in IQ or Message stanza, and process them.
|
||||
* @param stanza IQ response
|
||||
*/
|
||||
_handleRetractations (stanza) {
|
||||
// Note: here we don't know the object type. We must try on each collection.
|
||||
const ids = sizzle('', stanza).map(i => i.getAttribute('id'))
|
||||
for (const id of ids) {
|
||||
for (const key in this.types) {
|
||||
const type = this.types[key]
|
||||
const item = type.collection.get(id)
|
||||
if (!item) { continue }
|
||||
console.log('Removing Item ' + id + ' that was found in collection ' + key)
|
||||
type.collection.remove(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_parseItem (itemNode, type) {
|
||||
const data = {}
|
||||
if (type.fields) {
|
||||
for (const child of itemNode.children ?? []) {
|
||||
const fieldName = child.tagName
|
||||
if (!(fieldName in type.fields)) {
|
||||
continue
|
||||
}
|
||||
data[fieldName] = this._readValue(child.textContent, type.fields[fieldName])
|
||||
}
|
||||
}
|
||||
if (type.attributes) {
|
||||
for (const attr in type.attributes) {
|
||||
const value = itemNode.getAttribute(attr)
|
||||
if (attr !== undefined) {
|
||||
data[attr] = this._readValue(value, type.attributes[attr])
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
_readValue (v, t) {
|
||||
switch (t) {
|
||||
case String: return (v ?? '').toString()
|
||||
case Number: return parseInt(v) // only integers?
|
||||
case Boolean: return !!v
|
||||
default: return v // dont know what to do
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@
|
||||
-- Implemented nodes:
|
||||
-- * livechat-tasks: contains tasklist and task items, specific to livechat plugin.
|
||||
|
||||
-- TODO: add disco support.
|
||||
|
||||
local pubsub = require "util.pubsub";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
@ -125,7 +127,7 @@ local function simple_itemstore(room_jid)
|
||||
end
|
||||
end
|
||||
|
||||
local function get_broadcaster(room_jid)
|
||||
local function get_broadcaster(room_jid, room_host)
|
||||
local function simple_broadcast(kind, node, jids, item, _, node_obj)
|
||||
if node_obj then
|
||||
if node_obj.config["notify_"..kind] == false then
|
||||
@ -146,7 +148,9 @@ local function get_broadcaster(room_jid)
|
||||
end
|
||||
|
||||
local id = new_id();
|
||||
local message = st.message({ from = room_jid, type = "headline", id = id })
|
||||
-- FIXME: should we add a type=headline to the message? (this is what mod_pep does,
|
||||
-- and it seems that ConverseJS prefer to have it for server messages.)
|
||||
local message = st.message({ from = jid_join(room_jid, room_host), type = "headline", id = id })
|
||||
:tag("event", { xmlns = xmlns_pubsub_event })
|
||||
:tag(kind, { node = node });
|
||||
|
||||
@ -232,7 +236,7 @@ function get_mep_service(room_jid, room_host)
|
||||
|
||||
nodestore = nodestore(room_jid);
|
||||
itemstore = simple_itemstore(room_jid);
|
||||
broadcaster = get_broadcaster(room_jid);
|
||||
broadcaster = get_broadcaster(room_jid, room_host);
|
||||
-- subscriber_filter = get_subscriber_filter(room_jid);
|
||||
itemcheck = is_item_stanza;
|
||||
get_affiliation = function (jid)
|
||||
|
Loading…
Reference in New Issue
Block a user