一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
182 行
4.5 KiB
TypeScript
182 行
4.5 KiB
TypeScript
import { State as RootState } from '../../reducer';
|
|
import { Draft } from 'immer';
|
|
import { FolderEntity } from '../database/types';
|
|
import { MasterKeyEntity } from '../e2ee/types';
|
|
import Logger from '@joplin/utils/Logger';
|
|
|
|
const logger = Logger.create('share/reducer');
|
|
|
|
export enum ShareType {
|
|
Note = 1,
|
|
Folder = 3,
|
|
PublishedFolder = 4,
|
|
}
|
|
|
|
interface StateShareUserUser {
|
|
id: string;
|
|
email: string;
|
|
full_name?: string;
|
|
}
|
|
|
|
export enum ShareUserStatus {
|
|
Waiting = 0,
|
|
Accepted = 1,
|
|
Rejected = 2,
|
|
}
|
|
|
|
export interface SharePermissions {
|
|
can_read: number;
|
|
can_write: number;
|
|
}
|
|
|
|
export interface StateShareUser {
|
|
id: string;
|
|
status: ShareUserStatus;
|
|
user: StateShareUserUser;
|
|
can_read: number;
|
|
can_write: number;
|
|
}
|
|
|
|
export interface StateShare {
|
|
id: string;
|
|
type: number;
|
|
folder_id: string;
|
|
note_id: string;
|
|
master_key_id: string;
|
|
user?: StateShareUserUser;
|
|
}
|
|
|
|
export interface ShareInvitation {
|
|
id: string;
|
|
master_key: MasterKeyEntity;
|
|
share: StateShare;
|
|
status: ShareUserStatus;
|
|
can_read: number;
|
|
can_write: number;
|
|
}
|
|
|
|
export interface State {
|
|
shares: StateShare[];
|
|
shareUsers: Record<string, StateShareUser[]>;
|
|
shareInvitations: ShareInvitation[];
|
|
processingShareInvitationResponse: boolean;
|
|
}
|
|
|
|
export const stateRootKey = 'shareService';
|
|
|
|
export const defaultState: State = {
|
|
shares: [],
|
|
shareUsers: {},
|
|
shareInvitations: [],
|
|
processingShareInvitationResponse: false,
|
|
};
|
|
|
|
export const parseShareCache = (serialized: string): State => {
|
|
let raw: Partial<State> = {};
|
|
try {
|
|
raw = JSON.parse(serialized) || {};
|
|
} catch (error) {
|
|
logger.info('Could not load share cache from settings - will return a default value. Error was:', error);
|
|
}
|
|
|
|
return {
|
|
shares: raw.shares || [],
|
|
shareUsers: raw.shareUsers || {},
|
|
shareInvitations: raw.shareInvitations || [],
|
|
processingShareInvitationResponse: false,
|
|
};
|
|
};
|
|
|
|
export function isSharedFolderOwner(state: RootState, folderId: string): boolean {
|
|
const userId = state.settings['sync.userId'];
|
|
const share = state[stateRootKey].shares.find(s => s.folder_id === folderId);
|
|
if (!share) return false;
|
|
return share.user.id === userId;
|
|
}
|
|
|
|
export function isRootSharedFolder(folder: FolderEntity): boolean {
|
|
if (!('share_id' in folder) || !('parent_id' in folder)) {
|
|
logger.warn('Calling isRootSharedFolder without specifying share_id and parent_id:', folder);
|
|
return false;
|
|
}
|
|
|
|
return !!folder.share_id && !folder.parent_id;
|
|
}
|
|
|
|
// Returns true if folderId itself, or any of its ancestors, is directly published.
|
|
export function isFolderPublished(state: RootState, folderId: string): boolean {
|
|
const publishedIds = new Set(
|
|
state[stateRootKey].shares
|
|
.filter(s => s.type === ShareType.PublishedFolder && !!s.folder_id)
|
|
.map(s => s.folder_id),
|
|
);
|
|
const visitedFolderIds = new Set<string>();
|
|
let currentId = folderId;
|
|
while (currentId) {
|
|
if (publishedIds.has(currentId)) return true;
|
|
if (visitedFolderIds.has(currentId)) break;
|
|
visitedFolderIds.add(currentId);
|
|
const folder = state.folders.find(f => f.id === currentId);
|
|
if (!folder) break;
|
|
currentId = folder.parent_id;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Heterogeneous redux action shape across SHARE_* types; typed action union would require touching every dispatch site
|
|
const reducer = (draftRoot: Draft<RootState>, action: any) => {
|
|
if (action.type.indexOf('SHARE_') !== 0) return;
|
|
|
|
const draft = draftRoot.shareService;
|
|
|
|
try {
|
|
switch (action.type) {
|
|
|
|
case 'SHARE_SET':
|
|
|
|
draft.shares = action.shares;
|
|
break;
|
|
|
|
case 'SHARE_USER_SET':
|
|
|
|
draft.shareUsers[action.shareId] = action.shareUsers;
|
|
break;
|
|
|
|
case 'SHARE_USER_UPDATE_ONE':
|
|
|
|
{
|
|
const shareUser = draft.shareUsers[action.shareId].find((su: StateShareUser) => su.id === action.shareUser.id);
|
|
if (!shareUser) throw new Error(`No such user: ${JSON.stringify(action)}`);
|
|
|
|
for (const [name, value] of Object.entries(action.shareUser)) {
|
|
(shareUser as Record<string, unknown>)[name] = value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'SHARE_INVITATION_SET':
|
|
|
|
draft.shareInvitations = action.shareInvitations;
|
|
break;
|
|
|
|
case 'SHARE_CACHE_RESTORE':
|
|
|
|
draft.shares = action.shares;
|
|
draft.shareUsers = action.shareUsers;
|
|
draft.shareInvitations = action.shareInvitations;
|
|
break;
|
|
|
|
case 'SHARE_INVITATION_RESPONSE_PROCESSING':
|
|
|
|
draft.processingShareInvitationResponse = action.value;
|
|
break;
|
|
|
|
}
|
|
} catch (error) {
|
|
error.message = `In share reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export default reducer;
|