Require a room ID for WidgetStore's pinned widget contracts

This should alleviate https://github.com/vector-im/element-web/issues/15705 from happening, though the cause is still unknown.

Requiring a room ID is safe for this because only room widgets can be pinned, and widget IDs are not globally unique which means from a logical standpoint the contract still makes sense here.
This commit is contained in:
Travis Ralston 2020-12-01 13:05:48 -07:00
parent 2d74bb0dcc
commit bd1de8d45b
5 changed files with 24 additions and 26 deletions

View file

@ -57,7 +57,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
let unpinButton; let unpinButton;
if (showUnpin) { if (showUnpin) {
const onUnpinClick = () => { const onUnpinClick = () => {
WidgetStore.instance.unpinWidget(app.id); WidgetStore.instance.unpinWidget(room.roomId, app.id);
onFinished(); onFinished();
}; };
@ -143,7 +143,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
let moveLeftButton; let moveLeftButton;
if (showUnpin && widgetIndex > 0) { if (showUnpin && widgetIndex > 0) {
const onClick = () => { const onClick = () => {
WidgetStore.instance.movePinnedWidget(app.id, -1); WidgetStore.instance.movePinnedWidget(roomId, app.id, -1);
onFinished(); onFinished();
}; };
@ -153,7 +153,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
let moveRightButton; let moveRightButton;
if (showUnpin && widgetIndex < pinnedWidgets.length - 1) { if (showUnpin && widgetIndex < pinnedWidgets.length - 1) {
const onClick = () => { const onClick = () => {
WidgetStore.instance.movePinnedWidget(app.id, 1); WidgetStore.instance.movePinnedWidget(roomId, app.id, 1);
onFinished(); onFinished();
}; };

View file

@ -35,7 +35,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender(); const senderName = this.props.mxEvent.sender?.name || this.props.mxEvent.getSender();
let joinCopy = _t('Join the conference at the top of this room'); let joinCopy = _t('Join the conference at the top of this room');
if (!WidgetStore.instance.isPinned(this.props.mxEvent.getStateKey())) { if (!WidgetStore.instance.isPinned(this.props.mxEvent.getRoomId(), this.props.mxEvent.getStateKey())) {
joinCopy = _t('Join the conference from the room information card on the right'); joinCopy = _t('Join the conference from the room information card on the right');
} }

View file

@ -83,9 +83,10 @@ export const useWidgets = (room: Room) => {
interface IAppRowProps { interface IAppRowProps {
app: IApp; app: IApp;
room: Room;
} }
const AppRow: React.FC<IAppRowProps> = ({ app }) => { const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
const name = WidgetUtils.getWidgetName(app); const name = WidgetUtils.getWidgetName(app);
const dataTitle = WidgetUtils.getWidgetDataTitle(app); const dataTitle = WidgetUtils.getWidgetDataTitle(app);
const subtitle = dataTitle && " - " + dataTitle; const subtitle = dataTitle && " - " + dataTitle;
@ -100,10 +101,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
}); });
}; };
const isPinned = WidgetStore.instance.isPinned(app.id); const isPinned = WidgetStore.instance.isPinned(room.roomId, app.id);
const togglePin = isPinned const togglePin = isPinned
? () => { WidgetStore.instance.unpinWidget(app.id); } ? () => { WidgetStore.instance.unpinWidget(room.roomId, app.id); }
: () => { WidgetStore.instance.pinWidget(app.id); }; : () => { WidgetStore.instance.pinWidget(room.roomId, app.id); };
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
let contextMenu; let contextMenu;
@ -118,7 +119,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app }) => {
/>; />;
} }
const cannotPin = !isPinned && !WidgetStore.instance.canPin(app.id); const cannotPin = !isPinned && !WidgetStore.instance.canPin(room.roomId, app.id);
let pinTitle: string; let pinTitle: string;
if (cannotPin) { if (cannotPin) {
@ -183,7 +184,7 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
}; };
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}> return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Widgets")}>
{ apps.map(app => <AppRow key={app.id} app={app} />) } { apps.map(app => <AppRow key={app.id} app={app} room={room} />) }
<AccessibleButton kind="link" onClick={onManageIntegrations}> <AccessibleButton kind="link" onClick={onManageIntegrations}>
{ apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") } { apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") }

View file

@ -42,7 +42,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
const apps = useWidgets(room); const apps = useWidgets(room);
const app = apps.find(a => a.id === widgetId); const app = apps.find(a => a.id === widgetId);
const isPinned = app && WidgetStore.instance.isPinned(app.id); const isPinned = app && WidgetStore.instance.isPinned(room.roomId, app.id);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();

View file

@ -163,44 +163,42 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_EVENT); this.emit(UPDATE_EVENT);
}; };
public isPinned(widgetId: string) { public isPinned(roomId: string, widgetId: string) {
const roomId = this.getRoomId(widgetId);
return !!this.getPinnedApps(roomId).find(w => w.id === widgetId); return !!this.getPinnedApps(roomId).find(w => w.id === widgetId);
} }
public canPin(widgetId: string) { // dev note: we don't need the widgetId on this function, but the contract makes more sense
const roomId = this.getRoomId(widgetId); // when we require it.
public canPin(roomId: string, widgetId: string) {
return this.getPinnedApps(roomId).length < MAX_PINNED; return this.getPinnedApps(roomId).length < MAX_PINNED;
} }
public pinWidget(widgetId: string) { public pinWidget(roomId: string, widgetId: string) {
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId); const roomInfo = this.getRoom(roomId);
if (!roomInfo) return; if (!roomInfo) return;
// When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct // When pinning, first confirm all the widgets (Jitsi) which were autopinned so that the order is correct
const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]); const autoPinned = this.getPinnedApps(roomId).filter(app => !roomInfo.pinned[app.id]);
autoPinned.forEach(app => { autoPinned.forEach(app => {
this.setPinned(app.id, true); this.setPinned(roomId, app.id, true);
}); });
this.setPinned(widgetId, true); this.setPinned(roomId, widgetId, true);
// Show the apps drawer upon the user pinning a widget // Show the apps drawer upon the user pinning a widget
if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) { if (RoomViewStore.getRoomId() === this.getRoomId(widgetId)) {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "appsDrawer", action: "appsDrawer",
show: true, show: true,
}) });
} }
} }
public unpinWidget(widgetId: string) { public unpinWidget(roomId: string, widgetId: string) {
this.setPinned(widgetId, false); this.setPinned(roomId, widgetId, false);
} }
private setPinned(widgetId: string, value: boolean) { private setPinned(roomId: string, widgetId: string, value: boolean) {
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId); const roomInfo = this.getRoom(roomId);
if (!roomInfo) return; if (!roomInfo) return;
if (roomInfo.pinned[widgetId] === false && value) { if (roomInfo.pinned[widgetId] === false && value) {
@ -221,9 +219,8 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
this.emit(UPDATE_EVENT); this.emit(UPDATE_EVENT);
} }
public movePinnedWidget(widgetId: string, delta: 1 | -1) { public movePinnedWidget(roomId: string, widgetId: string, delta: 1 | -1) {
// TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away // TODO simplify this by changing the storage medium of pinned to an array once the Jitsi default-on goes away
const roomId = this.getRoomId(widgetId);
const roomInfo = this.getRoom(roomId); const roomInfo = this.getRoom(roomId);
if (!roomInfo || roomInfo.pinned[widgetId] === false) return; if (!roomInfo || roomInfo.pinned[widgetId] === false) return;