Ctrl K
ComunidadGuías…

app/models/User.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { subMinutes } from "date-fns";
import { computed, action, observable } from "mobx";
import { now } from "mobx-utils";
import { UserPreferenceDefaults } from "@shared/constants";
import {
  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 { unicodeCLDRtoBCP47 } 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 NavigableModel from "./base/NavigableModel";
import ParanoidModel from "./base/ParanoidModel";
import Field from "./decorators/Field";
import Relation from "./decorators/Relation";
import type { Searchable } from "./interfaces/Searchable";

class User extends ParanoidModel implements Searchable {
  static modelName = "User";

  constructor(fields: Record<string, unknown>, store: ParanoidModel["store"]) {
    super(fields, store);
    this.initialize(fields);
  }

  @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 = undefined;
  @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.
   *
   * This accessor is not computed because `now` must use the caller's reactive
   * context. This lets observed UI reads subscribe to clock updates and lets
   * untracked model updates read the current time without a subscription.
   */
  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;

  @observable
  invitedById: string | undefined = undefined;

  /** The user that invited this user, if they were invited. */
  @Relation(() => User)
  invitedBy: User | undefined;

  @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);
  }

  /**
   * The current time where the user is located, formatted for the locale of the
   * signed-in user.
   *
   * @returns the formatted time, or undefined if the user's timezone is unknown
   */
  @computed
  get localTime(): string | undefined {
    if (!this.timezone) {
      return undefined;
    }

    const language = this.store.rootStore.auth?.user?.language;

    try {
      return new Date(now(60000)).toLocaleTimeString(
        language ? unicodeCLDRtoBCP47(language) : undefined,
        { hour: "numeric", minute: "numeric", timeZone: this.timezone }
      );
    } catch {
      return undefined;
    }
  }

  /**
   * 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.
   * Documents nested under another shared document in the list are also not included.
   *
   * @returns A list of user memberships
   */
  @computed
  get documentMemberships(): UserMembership[] {
    const { userMemberships, documents, policies } = this.store.rootStore;
    const memberships = 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;
      });

    return NavigableModel.filterRoots(memberships, documents);
  }

  @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
   * @returns The current preference
   */
  public subscribedToEventType = (type: NotificationEventType) =>
    this.notificationSettings[type] ?? NotificationEventDefaults[type] ?? false;

  /**
   * 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
   */
  @action
  setNotificationEventType = async (
    eventType: NotificationEventType,
    value: boolean
  ) => {
    this.notificationSettings = {
      ...this.notificationSettings,
      [eventType]: value,
    };

    if (value) {
      await client.post(`/users.notificationsSubscribe`, {
        eventType,
      });
    } else {
      await client.post(`/users.notificationsUnsubscribe`, {
        eventType,
      });
    }
  };

  /**
   * 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;