You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

91 lines
3.6 KiB
JavaScript

"use strict";
const defaultValue = require("default-value");
const asExpression = require("as-expression");
const pipe = require("@promistream/pipe");
const map = require("@promistream/map");
const sequentialize = require("@promistream/sequentialize");
// FIXME: Accept initial state
module.exports = function createUserProfileTrackingStream() {
let currentUserProfiles = new Map( /* userID -> profileData */ );
function getUserProfileData(userID) {
return defaultValue(currentUserProfiles.get(userID), {});
// return currentUserProfiles.get(userID);
}
function setUserProfileData(userID, newProfileData) {
let currentProfileData = getUserProfileData(userID);
currentUserProfiles.set(userID, {
... currentProfileData,
... newProfileData
});
}
// FIXME: This linear approach does not work for backlog retrieval!
return {
stream: function () {
return pipe([
map((item) => {
// FIXME: Track per individual room, always sort roomStateUpdate before roomTimelineEvent
if (item.event != null && ["roomTimelineEvent", "roomStateUpdate"].includes(item.type)) {
let sender = item.event.sender;
let knownProfileData = getUserProfileData(sender);
// This is hacky, but needed to deal with the edgecase where a user's first event in the response is a profile change; in that case, the server won't have sent a roomStateUpdate for this user's previous membership, and so we need to backfill from the prev_content. We can't use the mapped events here because avatar and display name changes arrive separately, and so we can't fully process the former until the latter has been handled, which is of course impossible.
let profileData = asExpression(() => {
let isProfileEvent = ["userChangedAvatar", "userChangedDisplayName"].includes(item.event.type);
let hasPreviousState = (item.protocolEvent.unsigned.prev_content != null);
if (isProfileEvent && hasPreviousState) {
return {
avatarURL: defaultValue(knownProfileData.avatarURL, item.protocolEvent.unsigned.prev_content.avatar_url),
displayName: defaultValue(knownProfileData.displayName, item.protocolEvent.unsigned.prev_content.displayname),
};
} else {
return knownProfileData;
}
});
// We only attach profile data to roomTimelineEvents, since roomStateUpdates are conceptually unordered and should not be displayed anyway.
let newItem = (item.type === "roomTimelineEvent")
? {
... item,
event: {
... item.event,
userProfile: profileData
}
}
: item;
// NOTE: We update the tracked profile data *after* generating the modified item, because at the time of a profile-data-change event, the old profile data still applies. The new profile data only conceptually starts applying *after* that event.
if (item.event.type === "userChangedAvatar") {
setUserProfileData(sender, { avatarURL: item.event.url });
} else if (item.event.type === "userChangedDisplayName") {
setUserProfileData(sender, { displayName: item.event.name });
// console.log(`Display name change for ${(newItem.event.userProfile || {}).displayName}: ${newItem.event.previousName} -> ${newItem.event.name}`);
}
// MARKER: f0x -> f0x displayname change // "$15948192607987cjWRn:pixie.town_0" // "!XTcFGXYoSwwJPindtt:feneas.org"
if (item.event.sender.includes("f0x")) {
// console.log("f0x", newItem);
}
return newItem;
} else {
return item;
}
}),
sequentialize()
]);
},
getCurrentState: function () {
return currentUserProfiles;
}
};
};