feat(presence): ajouter la gestion du statut de présence manuel dans le menu utilisateur

mbarbe-develop
dlamarcheteamnet 1 week ago
parent 6e8a01c811
commit 08035c4e45
  1. 29
      res/css/structures/_UserMenu.pcss
  2. 82
      src/Presence.ts
  3. 44
      src/components/structures/UserMenu.tsx
  4. 2
      src/i18n/strings/en_EN.json
  5. 2
      src/i18n/strings/fr.json
  6. 6
      src/settings/Settings.tsx

@ -228,6 +228,35 @@ limitations under the License.
.mx_UserMenu_iconJitsi::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
// Sélecteur de statut : pastilles de couleur (disponible / occupé / absent / automatique)
.mx_UserMenu_iconStatusAvailable::before,
.mx_UserMenu_iconStatusBusy::before,
.mx_UserMenu_iconStatusAway::before,
.mx_UserMenu_iconStatusAuto::before {
mask: none;
-webkit-mask: none;
width: 10px;
height: 10px;
margin: 3px;
border-radius: 50%;
}
.mx_UserMenu_iconStatusAvailable::before {
background-color: #2dbd59; // vert : disponible
}
.mx_UserMenu_iconStatusBusy::before {
background-color: #ff5b55; // rouge : occupé
}
.mx_UserMenu_iconStatusAway::before {
background-color: #fdbd64; // orange : absent
}
.mx_UserMenu_iconStatusAuto::before {
background-color: $secondary-content; // gris : détection automatique
}
// +watcha
}

@ -23,14 +23,30 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher";
import Timer from "./utils/Timer";
import { ActionPayload } from "./dispatcher/payloads";
import SettingsStore from "./settings/SettingsStore"; // watcha+
import { SettingLevel } from "./settings/SettingLevel"; // watcha+
// Time in ms after that a user is considered as unavailable/away
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
// watcha+
// Statut de présence pouvant être choisi manuellement par l'utilisateur.
// "online" et "unavailable" sont des états standards Matrix ; "busy" relève de MSC3026
// (org.matrix.msc3026.busy) et doit être activé côté Synapse (experimental_features.msc3026_enabled).
export type ManualPresence = "online" | "unavailable" | "org.matrix.msc3026.busy";
export const ManualPresence = {
Available: "online",
Away: "unavailable",
Busy: "org.matrix.msc3026.busy",
} as const satisfies Record<string, ManualPresence>;
const MANUAL_PRESENCE_SETTING = "watcha_manualPresence";
// +watcha
class Presence {
private unavailableTimer: Timer | null = null;
private dispatcherRef: string | null = null;
private state: SetPresence | null = null;
private manualPresence: ManualPresence | null = null; // watcha+
/**
* Start listening the user activity to evaluate his presence state.
@ -40,9 +56,20 @@ class Presence {
this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
// the user_activity_start action starts the timer
this.dispatcherRef = dis.register(this.onAction);
// watcha+
// Réapplique un éventuel statut choisi manuellement lors d'une session précédente,
// afin qu'il survive à un rechargement de la page.
const savedPresence = SettingsStore.getValue(MANUAL_PRESENCE_SETTING) as ManualPresence | null;
if (savedPresence) {
await this.setManualPresence(savedPresence, { persist: false });
}
// +watcha
while (this.unavailableTimer) {
try {
await this.unavailableTimer.finished();
// watcha+ : ne pas écraser un statut choisi manuellement par l'utilisateur
if (this.manualPresence) continue;
// +watcha
this.setState(SetPresence.Unavailable);
} catch (e) {
/* aborted, stop got called */
@ -74,11 +101,66 @@ class Presence {
private onAction = (payload: ActionPayload): void => {
if (payload.action === "user_activity") {
// watcha+ : un statut manuel verrouille la détection automatique
if (this.manualPresence) return;
// +watcha
this.setState(SetPresence.Online);
this.unavailableTimer?.restart();
}
};
// watcha+
/**
* Get the presence state explicitly chosen by the user, if any.
* @returns the manual presence (disponible / absent / occupé) or null when in automatic mode.
*/
public getManualPresence(): ManualPresence | null {
return this.manualPresence;
}
/**
* Set (or clear) the presence state explicitly chosen by the user.
* While a manual presence is set, the automatic activity-based detection is suspended.
* @param presence the chosen presence, or null to resume automatic detection.
* @param persist whether to persist the choice so it survives a page reload (default: true).
*/
public async setManualPresence(presence: ManualPresence | null, { persist = true } = {}): Promise<void> {
this.manualPresence = presence;
if (persist) {
await SettingsStore.setValue(MANUAL_PRESENCE_SETTING, null, SettingLevel.DEVICE, presence);
}
if (presence === null) {
// Reprise du mode automatique : on repasse en ligne et on réarme le minuteur d'inactivité.
this.state = null;
await this.setState(SetPresence.Online);
this.unavailableTimer?.restart();
return;
}
if (MatrixClientPeg.safeGet().isGuest()) {
return; // don't try to set presence when a guest; it won't work.
}
// Le paramètre set_presence des appels /sync n'accepte que online/unavailable/offline.
// L'état "busy" (MSC3026) est conservé côté serveur et ne doit donc être poussé que via
// l'API /presence/{userId}/status, sans modifier la présence de synchronisation.
if (presence === ManualPresence.Away) {
await this.setState(SetPresence.Unavailable);
} else if (presence === ManualPresence.Available) {
await this.setState(SetPresence.Online);
}
try {
await MatrixClientPeg.safeGet().setPresence({ presence });
logger.debug("Manual presence:", presence);
} catch (err) {
logger.error("Failed to set manual presence:", err);
}
}
// +watcha
/**
* Set the presence state.
* If the state has changed, the homeserver will be notified.

@ -41,6 +41,7 @@ import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
IconizedContextMenuRadio, // watcha+
} from "../views/context_menus/IconizedContextMenu";
import { UIFeature } from "../../settings/UIFeature";
import SpaceStore from "../../stores/spaces/SpaceStore";
@ -50,6 +51,7 @@ import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import PosthogTrackers from "../../PosthogTrackers";
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
import { getNextcloudBaseUrl } from "../../utils/watcha_nextcloudUtils"; // watcha+
import Presence, { ManualPresence } from "../../Presence"; // watcha+
import { Jitsi } from "../../widgets/Jitsi"; // watcha+
import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg";
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
@ -377,6 +379,15 @@ export default class UserMenu extends React.Component<IProps, IState> {
window.open(emailBaseUrl);
this.setState({ contextMenuPosition: null }); // also close the menu
};
// Statut de présence choisi manuellement (disponible / absent / occupé) ou retour au mode automatique.
private onPresenceClick = (ev: ButtonEvent, presence: ManualPresence | null): void => {
ev.preventDefault();
ev.stopPropagation();
Presence.setManualPresence(presence);
this.setState({ contextMenuPosition: null }); // also close the menu
};
// +watcha
private renderContextMenu = (): React.ReactNode => {
@ -459,8 +470,41 @@ export default class UserMenu extends React.Component<IProps, IState> {
);
}
// watcha+ : sélecteur de statut de présence (disponible / absent / occupé)
const currentManualPresence = Presence.getManualPresence();
const statusOptionList = (
<IconizedContextMenuOptionList label={_t("watcha|status_title")}>
<IconizedContextMenuRadio
iconClassName="mx_UserMenu_iconStatusAvailable"
label={_t("presence|online")}
active={currentManualPresence === ManualPresence.Available}
onClick={(e) => this.onPresenceClick(e, ManualPresence.Available)}
/>
<IconizedContextMenuRadio
iconClassName="mx_UserMenu_iconStatusBusy"
label={_t("presence|busy")}
active={currentManualPresence === ManualPresence.Busy}
onClick={(e) => this.onPresenceClick(e, ManualPresence.Busy)}
/>
<IconizedContextMenuRadio
iconClassName="mx_UserMenu_iconStatusAway"
label={_t("presence|away")}
active={currentManualPresence === ManualPresence.Away}
onClick={(e) => this.onPresenceClick(e, ManualPresence.Away)}
/>
<IconizedContextMenuRadio
iconClassName="mx_UserMenu_iconStatusAuto"
label={_t("watcha|status_automatic")}
active={currentManualPresence === null}
onClick={(e) => this.onPresenceClick(e, null)}
/>
</IconizedContextMenuOptionList>
);
// +watcha
let primaryOptionList = (
<> { /* eslint-disable indent *//* watcha+ */ }
{statusOptionList}
<IconizedContextMenuOptionList>
{homeButton}
{linkNewDeviceButton}

@ -4031,6 +4031,8 @@
"show_documents": "Show documents shared with the room",
"show_task": "Show the to-do list shared with the room",
"sso_profile": "SSO Profile",
"status_automatic": "Automatic",
"status_title": "Status",
"stop_sharing": "Stop sharing",
"stop_sharing_description": "Please note that only the owner of this resource will be able to share it again.",
"stop_sharing_title": "Are you sure you want to end this sharing?",

@ -4033,6 +4033,8 @@
"show_documents": "Afficher les documents partagés avec le salon",
"show_task": "Afficher la liste de tâches partagée avec le salon",
"sso_profile": "Profil SSO",
"status_automatic": "Automatique",
"status_title": "Statut",
"stop_sharing": "Arrêter de partager",
"stop_sharing_description": "Veuillez noter que seul le propriétaire de cette ressource pourra la partager à nouveau.",
"stop_sharing_title": "Êtes-vous sûr de vouloir mettre fin à ce partage ?",

@ -1276,6 +1276,12 @@ export const SETTINGS: { [setting: string]: ISetting } = {
default: true,
},
// watcha+
// Statut de présence choisi manuellement par l'utilisateur (disponible / absent / occupé).
// null => détection automatique de la présence (comportement par défaut d'Element).
"watcha_manualPresence": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: null,
},
[UIFeature.watcha_Administration]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,

Loading…
Cancel
Save