mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
326 lines
8.0 KiB
TypeScript
326 lines
8.0 KiB
TypeScript
import { subMinutes } from "date-fns";
|
|
import { computed, action, observable } from "mobx";
|
|
import { now } from "mobx-utils";
|
|
import { UserPreferenceDefaults } from "@shared/constants";
|
|
import {
|
|
NotificationChannelType,
|
|
NotificationEventDefaults,
|
|
type NotificationEventType,
|
|
TeamPreference,
|
|
UserPreference,
|
|
type UserPreferences,
|
|
UserRole,
|
|
} from "@shared/types";
|
|
import type { NotificationSettings } from "@shared/types";
|
|
import type { locales } from "@shared/utils/date";
|
|
import { client } from "~/utils/ApiClient";
|
|
import type Document from "./Document";
|
|
import type Group from "./Group";
|
|
import type UserMembership from "./UserMembership";
|
|
import ParanoidModel from "./base/ParanoidModel";
|
|
import Field from "./decorators/Field";
|
|
import type { Searchable } from "./interfaces/Searchable";
|
|
|
|
class User extends ParanoidModel implements Searchable {
|
|
static modelName = "User";
|
|
|
|
@Field
|
|
@observable
|
|
avatarUrl: string;
|
|
|
|
@Field
|
|
@observable
|
|
name: string;
|
|
|
|
@Field
|
|
@observable
|
|
color: string;
|
|
|
|
@Field
|
|
@observable
|
|
language: keyof typeof locales;
|
|
|
|
@Field
|
|
@observable
|
|
preferences: UserPreferences | null;
|
|
|
|
@Field
|
|
@observable
|
|
notificationSettings: NotificationSettings;
|
|
|
|
@Field
|
|
@observable
|
|
timezone?: string;
|
|
|
|
@observable
|
|
email: string;
|
|
|
|
@observable
|
|
role: UserRole;
|
|
|
|
@observable
|
|
protected _lastActiveAt: string;
|
|
|
|
/**
|
|
* The last time the user was active. For the currently signed-in user, this
|
|
* always returns the current date so they always appear as recently active.
|
|
*/
|
|
@computed
|
|
get lastActiveAt(): string {
|
|
if (this.store.rootStore.auth?.currentUserId === this.id) {
|
|
return new Date(now(60000)).toISOString();
|
|
}
|
|
return this._lastActiveAt;
|
|
}
|
|
|
|
set lastActiveAt(value: string) {
|
|
this._lastActiveAt = value;
|
|
}
|
|
|
|
@observable
|
|
isSuspended: boolean;
|
|
|
|
@computed
|
|
get searchContent(): string[] {
|
|
return [this.name, this.email, this.initials].filter(Boolean);
|
|
}
|
|
|
|
@computed
|
|
get searchSuppressed(): boolean {
|
|
return this.isDeleted;
|
|
}
|
|
|
|
@computed
|
|
get initial(): string {
|
|
return (this.name ? this.name[0] : "?").toUpperCase();
|
|
}
|
|
|
|
@computed
|
|
get initials(): string {
|
|
if (!this.name) {
|
|
return "";
|
|
}
|
|
const names = this.name.trim().split(" ");
|
|
if (names.length === 1) {
|
|
return names[0][0].toUpperCase();
|
|
}
|
|
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Whether the user has been invited but not yet signed in.
|
|
*/
|
|
get isInvited(): boolean {
|
|
return !this.lastActiveAt;
|
|
}
|
|
|
|
/**
|
|
* Whether the user is an admin.
|
|
*/
|
|
get isAdmin(): boolean {
|
|
return this.role === UserRole.Admin;
|
|
}
|
|
|
|
/**
|
|
* Whether the user is a member (editor).
|
|
*/
|
|
get isMember(): boolean {
|
|
return this.role === UserRole.Member;
|
|
}
|
|
|
|
/**
|
|
* Whether the user is a viewer.
|
|
*/
|
|
get isViewer(): boolean {
|
|
return this.role === UserRole.Viewer;
|
|
}
|
|
|
|
/**
|
|
* Whether the user is a guest.
|
|
*/
|
|
get isGuest(): boolean {
|
|
return this.role === UserRole.Guest;
|
|
}
|
|
|
|
/**
|
|
* Whether the user has been recently active. Recently is currently defined
|
|
* as within the last 5 minutes.
|
|
*
|
|
* @returns true if the user has been active recently
|
|
*/
|
|
@computed
|
|
get isRecentlyActive(): boolean {
|
|
return new Date(this.lastActiveAt) > subMinutes(now(10000), 5);
|
|
}
|
|
|
|
/**
|
|
* Returns whether this user is using a separate editing mode behind an "Edit"
|
|
* button rather than seamless always-editing.
|
|
*
|
|
* @returns True if editing mode is seamless (no button)
|
|
*/
|
|
@computed
|
|
get separateEditMode(): boolean {
|
|
return !this.getPreference(
|
|
UserPreference.SeamlessEdit,
|
|
this.store.rootStore.auth?.team?.getPreference(
|
|
TeamPreference.SeamlessEdit
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the direct memberships that this user has to documents. Documents that the
|
|
* user already has access to through a collection, archived, and trashed documents are not included.
|
|
*
|
|
* @returns A list of user memberships
|
|
*/
|
|
@computed
|
|
get documentMemberships(): UserMembership[] {
|
|
const { userMemberships, documents, policies } = this.store.rootStore;
|
|
return userMemberships.orderedData
|
|
.filter(
|
|
(m) => m.userId === this.id && m.sourceId === null && m.documentId
|
|
)
|
|
.filter((m) => {
|
|
const document = documents.get(m.documentId!);
|
|
const policy = document?.collectionId
|
|
? policies.get(document.collectionId)
|
|
: undefined;
|
|
return !policy?.abilities?.readDocument && !!document?.isActive;
|
|
});
|
|
}
|
|
|
|
@computed
|
|
get groupsWithDocumentMemberships() {
|
|
const { groups, groupUsers } = this.store.rootStore;
|
|
|
|
return groupUsers.orderedData
|
|
.filter((groupUser) => groupUser.userId === this.id)
|
|
.map((groupUser) => groups.get(groupUser.groupId))
|
|
.filter(Boolean)
|
|
.filter((group) => group && group.documentMemberships.length > 0)
|
|
.sort((a, b) => a!.name.localeCompare(b!.name)) as Group[];
|
|
}
|
|
|
|
/**
|
|
* Returns the current preference for the given notification event type taking
|
|
* into account the default system value.
|
|
*
|
|
* @param type The type of notification event.
|
|
* @param channel Optional channel type for channel-specific check.
|
|
* @returns The current preference.
|
|
*/
|
|
public subscribedToEventType = (
|
|
type: NotificationEventType,
|
|
channel = NotificationChannelType.Email
|
|
): boolean => {
|
|
const setting = this.notificationSettings[type];
|
|
const defaultValue = NotificationEventDefaults[type] ?? false;
|
|
|
|
if (setting === undefined) {
|
|
return defaultValue;
|
|
}
|
|
|
|
if (typeof setting === "boolean") {
|
|
return setting;
|
|
}
|
|
|
|
if (typeof setting === "object") {
|
|
return setting[channel] ?? defaultValue;
|
|
}
|
|
|
|
return defaultValue;
|
|
};
|
|
|
|
/**
|
|
* Sets a preference for the users notification settings on the model and
|
|
* saves the change to the server.
|
|
*
|
|
* @param type The type of notification event.
|
|
* @param value Set the preference to true/false.
|
|
* @param channel Optional channel type for channel-specific settings.
|
|
*/
|
|
@action
|
|
setNotificationEventType = async (
|
|
eventType: NotificationEventType,
|
|
value: boolean | Record<NotificationChannelType, boolean>,
|
|
channel?: NotificationChannelType
|
|
) => {
|
|
if (channel !== undefined) {
|
|
// Setting a specific channel preference
|
|
const currentSetting = this.notificationSettings[eventType];
|
|
const channelSettings =
|
|
typeof currentSetting === "object" ? currentSetting : {};
|
|
|
|
this.notificationSettings = {
|
|
...this.notificationSettings,
|
|
[eventType]: {
|
|
...channelSettings,
|
|
[channel]: value,
|
|
},
|
|
};
|
|
} else {
|
|
// Setting all channels or simple boolean
|
|
this.notificationSettings = {
|
|
...this.notificationSettings,
|
|
[eventType]: value,
|
|
};
|
|
}
|
|
|
|
if (value) {
|
|
await client.post(`/users.notificationsSubscribe`, {
|
|
eventType,
|
|
channel,
|
|
});
|
|
} else {
|
|
await client.post(`/users.notificationsUnsubscribe`, {
|
|
eventType,
|
|
channel,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the value for a specific preference key, or return the fallback if
|
|
* none is set.
|
|
*
|
|
* @param key The UserPreference key to retrieve
|
|
* @returns The value
|
|
*/
|
|
getPreference<K extends UserPreference>(
|
|
key: K,
|
|
defaultValue?: UserPreferences[K]
|
|
): NonNullable<UserPreferences[K]> {
|
|
return (this.preferences?.[key] ??
|
|
UserPreferenceDefaults[key] ??
|
|
defaultValue ??
|
|
false) as NonNullable<UserPreferences[K]>;
|
|
}
|
|
|
|
/**
|
|
* Set the value for a specific preference key.
|
|
*
|
|
* @param key The UserPreference key to retrieve
|
|
* @param value The value to set
|
|
*/
|
|
@action
|
|
setPreference<K extends UserPreference>(
|
|
key: K,
|
|
value: NonNullable<UserPreferences[K]>
|
|
) {
|
|
this.preferences = {
|
|
...this.preferences,
|
|
[key]: value,
|
|
};
|
|
}
|
|
|
|
getMembership(document: Document) {
|
|
return this.store.rootStore.userMemberships.orderedData.find(
|
|
(m) => m.documentId === document.id && m.userId === this.id
|
|
);
|
|
}
|
|
}
|
|
|
|
export default User;
|