Files
fx-test/fluxer_app/src/stores/AutoAckStore.tsx
Hampus Kraft 2f557eda8c initial commit
2026-01-01 21:05:54 +00:00

172 lines
4.6 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {action, makeAutoObservable, reaction} from 'mobx';
import {Logger} from '~/lib/Logger';
import ChannelStore from './ChannelStore';
import DimensionStore from './DimensionStore';
import ReadStateStore from './ReadStateStore';
import SelectedChannelStore from './SelectedChannelStore';
import WindowStore from './WindowStore';
const logger = new Logger('AutoAckStore');
type ChannelId = string;
class AutoAckStore {
private readonly windowChannels = new Map<string, Set<ChannelId>>();
private readonly windowConditions = new Map<
string,
{
channelId: ChannelId | null;
isAtBottom: boolean;
canAutoAck: boolean;
}
>();
constructor() {
makeAutoObservable<this, 'windowChannels' | 'windowConditions'>(
this,
{
windowChannels: false,
windowConditions: false,
},
{autoBind: true},
);
this.setupReactions();
}
private setupReactions(): void {
reaction(
() => {
const windowId = WindowStore.windowId;
const channelId = SelectedChannelStore.currentChannelId;
const isWindowFocused = WindowStore.focused;
if (!channelId) {
return {windowId, channelId: null, isAtBottom: false, canAutoAck: false};
}
const isAtBottom = DimensionStore.isAtBottom(channelId) ?? false;
const readState = ReadStateStore.getIfExists(channelId);
const isManualAck = readState?.isManualAck ?? false;
const canAutoAck = !isManualAck && isWindowFocused;
return {windowId, channelId, isAtBottom, canAutoAck};
},
(conditions) => {
this.updateAutoAckState(conditions);
},
{
name: 'AutoAckStore.updateAutoAckState',
fireImmediately: true,
},
);
}
@action
private updateAutoAckState(conditions: {
windowId: string;
channelId: ChannelId | null;
isAtBottom: boolean;
canAutoAck: boolean;
}): void {
const {windowId, channelId, isAtBottom, canAutoAck} = conditions;
const prevConditions = this.windowConditions.get(windowId);
this.windowConditions.set(windowId, {channelId, isAtBottom, canAutoAck});
if (prevConditions?.channelId && prevConditions.channelId !== channelId) {
this.disableAutomaticAckInternal(prevConditions.channelId, windowId);
}
if (!channelId) {
return;
}
const shouldEnable = isAtBottom && canAutoAck;
if (shouldEnable) {
this.enableAutomaticAckInternal(channelId, windowId);
} else {
this.disableAutomaticAckInternal(channelId, windowId);
}
}
@action
private enableAutomaticAckInternal(channelId: ChannelId, windowId: string): void {
const channel = ChannelStore.getChannel(channelId);
if (channel == null) {
logger.debug(`Ignoring enableAutomaticAck for non-existent channel ${channelId}`);
return;
}
let channels = this.windowChannels.get(windowId);
if (channels == null) {
channels = new Set();
this.windowChannels.set(windowId, channels);
}
if (!channels.has(channelId)) {
channels.add(channelId);
logger.debug(`Enabled automatic ack for ${channelId} in window ${windowId}`);
}
}
@action
private disableAutomaticAckInternal(channelId: ChannelId, windowId: string): void {
const channels = this.windowChannels.get(windowId);
if (channels == null) return;
if (channels.has(channelId)) {
channels.delete(channelId);
logger.debug(`Disabled automatic ack for ${channelId} in window ${windowId}`);
}
if (channels.size === 0) {
this.windowChannels.delete(windowId);
}
}
isAutomaticAckEnabled(channelId: ChannelId): boolean {
for (const channels of this.windowChannels.values()) {
if (channels.has(channelId)) return true;
}
return false;
}
@action
disableForChannel(channelId: ChannelId): void {
for (const [windowId, channels] of this.windowChannels.entries()) {
if (channels.has(channelId)) {
channels.delete(channelId);
logger.debug(`Force-disabled automatic ack for ${channelId} in window ${windowId}`);
}
if (channels.size === 0) {
this.windowChannels.delete(windowId);
}
}
}
}
export default new AutoAckStore();