clipper: handle clipped note sync in background (#2367)
This commit is contained in:
@@ -181,7 +181,7 @@ export class HttpService implements HttpServiceInterface {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshSession(): Promise<boolean> {
|
async refreshSession(): Promise<boolean> {
|
||||||
if (!this.session) {
|
if (!this.session) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface HttpServiceInterface {
|
|||||||
runHttp<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>>
|
runHttp<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>>
|
||||||
|
|
||||||
setSession(session: Session | LegacySession): void
|
setSession(session: Session | LegacySession): void
|
||||||
|
refreshSession(): Promise<boolean>
|
||||||
setCallbacks(
|
setCallbacks(
|
||||||
updateMetaCallback: (meta: HttpResponseMeta) => void,
|
updateMetaCallback: (meta: HttpResponseMeta) => void,
|
||||||
refreshSessionCallback: (session: Session) => void,
|
refreshSessionCallback: (session: Session) => void,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './HttpService'
|
export * from './HttpService'
|
||||||
|
export * from './FetchRequestHandler'
|
||||||
export * from './HttpServiceInterface'
|
export * from './HttpServiceInterface'
|
||||||
export * from './XMLHttpRequestState'
|
export * from './XMLHttpRequestState'
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { runtime, action, browserAction, windows, storage, tabs } from 'webextension-polyfill'
|
import { runtime, action, browserAction, windows, storage, tabs } from 'webextension-polyfill'
|
||||||
import { ClipPayload, RuntimeMessage, RuntimeMessageTypes } from '../types/message'
|
import { ClipPayload, RuntimeMessage, RuntimeMessageTypes } from '../types/message'
|
||||||
|
import { Environment, FetchRequestHandler, Logger, SnjsVersion } from '@standardnotes/snjs'
|
||||||
|
import packageInfo from '../../package.json'
|
||||||
|
|
||||||
const isFirefox = navigator.userAgent.indexOf('Firefox/') !== -1
|
const isFirefox = navigator.userAgent.indexOf('Firefox/') !== -1
|
||||||
|
|
||||||
@@ -22,6 +24,9 @@ const openPopupAndClipSelection = async (payload: ClipPayload) => {
|
|||||||
void openPopup()
|
void openPopup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = new Logger('clipper')
|
||||||
|
const requestHandler = new FetchRequestHandler(SnjsVersion, packageInfo.version, Environment.Clipper, logger)
|
||||||
|
|
||||||
runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
||||||
if (message.type === RuntimeMessageTypes.OpenPopupWithSelection) {
|
if (message.type === RuntimeMessageTypes.OpenPopupWithSelection) {
|
||||||
if (!message.payload) {
|
if (!message.payload) {
|
||||||
@@ -32,5 +37,7 @@ runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
|||||||
return await tabs.captureVisibleTab(undefined, {
|
return await tabs.captureVisibleTab(undefined, {
|
||||||
format: 'png',
|
format: 'png',
|
||||||
})
|
})
|
||||||
|
} else if (message.type === RuntimeMessageTypes.RunHttpRequest) {
|
||||||
|
requestHandler.handleRequest(message.payload).catch(console.error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { HttpRequest } from '@standardnotes/snjs'
|
||||||
|
|
||||||
export const RuntimeMessageTypes = {
|
export const RuntimeMessageTypes = {
|
||||||
GetArticle: 'get-article',
|
GetArticle: 'get-article',
|
||||||
GetSelection: 'get-selection',
|
GetSelection: 'get-selection',
|
||||||
@@ -7,6 +9,7 @@ export const RuntimeMessageTypes = {
|
|||||||
StartNodeSelection: 'start-node-selection',
|
StartNodeSelection: 'start-node-selection',
|
||||||
ToggleScreenshotMode: 'toggle-screenshot-mode',
|
ToggleScreenshotMode: 'toggle-screenshot-mode',
|
||||||
CaptureVisibleTab: 'capture-visible-tab',
|
CaptureVisibleTab: 'capture-visible-tab',
|
||||||
|
RunHttpRequest: 'run-http-request',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type RuntimeMessageType = (typeof RuntimeMessageTypes)[keyof typeof RuntimeMessageTypes]
|
export type RuntimeMessageType = (typeof RuntimeMessageTypes)[keyof typeof RuntimeMessageTypes]
|
||||||
@@ -29,6 +32,7 @@ export type RuntimeMessageReturnTypes = {
|
|||||||
[RuntimeMessageTypes.OpenPopupWithSelection]: void
|
[RuntimeMessageTypes.OpenPopupWithSelection]: void
|
||||||
[RuntimeMessageTypes.StartNodeSelection]: void
|
[RuntimeMessageTypes.StartNodeSelection]: void
|
||||||
[RuntimeMessageTypes.ToggleScreenshotMode]: void
|
[RuntimeMessageTypes.ToggleScreenshotMode]: void
|
||||||
|
[RuntimeMessageTypes.RunHttpRequest]: void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeMessage =
|
export type RuntimeMessage =
|
||||||
@@ -36,10 +40,19 @@ export type RuntimeMessage =
|
|||||||
type: MessagesWithClipPayload
|
type: MessagesWithClipPayload
|
||||||
payload: ClipPayload
|
payload: ClipPayload
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: typeof RuntimeMessageTypes.RunHttpRequest
|
||||||
|
payload: HttpRequest
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: typeof RuntimeMessageTypes.ToggleScreenshotMode
|
type: typeof RuntimeMessageTypes.ToggleScreenshotMode
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: Exclude<RuntimeMessageType, MessagesWithClipPayload | typeof RuntimeMessageTypes.ToggleScreenshotMode>
|
type: Exclude<
|
||||||
|
RuntimeMessageType,
|
||||||
|
| MessagesWithClipPayload
|
||||||
|
| typeof RuntimeMessageTypes.ToggleScreenshotMode
|
||||||
|
| typeof RuntimeMessageTypes.RunHttpRequest
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export enum Environment {
|
|||||||
Web = 1,
|
Web = 1,
|
||||||
Desktop = 2,
|
Desktop = 2,
|
||||||
Mobile = 3,
|
Mobile = 3,
|
||||||
|
Clipper = 4,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { FilesApiInterface } from '@standardnotes/files'
|
|||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
import { ApiServiceEvent } from './ApiServiceEvent'
|
import { ApiServiceEvent } from './ApiServiceEvent'
|
||||||
import { ApiServiceEventData } from './ApiServiceEventData'
|
import { ApiServiceEventData } from './ApiServiceEventData'
|
||||||
import { SNFeatureRepo } from '@standardnotes/models'
|
import { SNFeatureRepo, ServerSyncPushContextualPayload } from '@standardnotes/models'
|
||||||
import { ClientDisplayableError, HttpResponse } from '@standardnotes/responses'
|
import { ClientDisplayableError, HttpRequest, HttpResponse } from '@standardnotes/responses'
|
||||||
import { AnyFeatureDescription } from '@standardnotes/features'
|
import { AnyFeatureDescription } from '@standardnotes/features'
|
||||||
|
|
||||||
export interface LegacyApiServiceInterface
|
export interface LegacyApiServiceInterface
|
||||||
@@ -16,4 +16,12 @@ export interface LegacyApiServiceInterface
|
|||||||
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
|
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
|
||||||
|
|
||||||
downloadFeatureUrl(url: string): Promise<HttpResponse>
|
downloadFeatureUrl(url: string): Promise<HttpResponse>
|
||||||
|
|
||||||
|
getSyncHttpRequest(
|
||||||
|
payloads: ServerSyncPushContextualPayload[],
|
||||||
|
lastSyncToken: string | undefined,
|
||||||
|
paginationToken: string | undefined,
|
||||||
|
limit: number,
|
||||||
|
sharedVaultUuids?: string[],
|
||||||
|
): HttpRequest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface SessionsClientInterface {
|
|||||||
isSignedIntoFirstPartyServer(): boolean
|
isSignedIntoFirstPartyServer(): boolean
|
||||||
|
|
||||||
getSessionsList(): Promise<HttpResponse<SessionListEntry[]>>
|
getSessionsList(): Promise<HttpResponse<SessionListEntry[]>>
|
||||||
|
refreshSessionIfExpiringSoon(): Promise<boolean>
|
||||||
revokeSession(sessionId: string): Promise<HttpResponse<SessionListResponse>>
|
revokeSession(sessionId: string): Promise<HttpResponse<SessionListResponse>>
|
||||||
revokeAllOtherSessions(): Promise<void>
|
revokeAllOtherSessions(): Promise<void>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
|
|
||||||
import { FullyFormedPayloadInterface } from '@standardnotes/models'
|
import { DecryptedItemInterface, DeletedItemInterface, FullyFormedPayloadInterface } from '@standardnotes/models'
|
||||||
import { SyncOptions } from './SyncOptions'
|
import { SyncOptions } from './SyncOptions'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
import { SyncEvent } from '../Event/SyncEvent'
|
import { SyncEvent } from '../Event/SyncEvent'
|
||||||
import { SyncOpStatus } from './SyncOpStatus'
|
import { SyncOpStatus } from './SyncOpStatus'
|
||||||
|
import { HttpRequest } from '@standardnotes/responses'
|
||||||
|
|
||||||
export interface SyncServiceInterface extends AbstractService<SyncEvent> {
|
export interface SyncServiceInterface extends AbstractService<SyncEvent> {
|
||||||
sync(options?: Partial<SyncOptions>): Promise<unknown>
|
sync(options?: Partial<SyncOptions>): Promise<unknown>
|
||||||
|
getRawSyncRequestForExternalUse(
|
||||||
|
items: (DecryptedItemInterface | DeletedItemInterface)[],
|
||||||
|
): Promise<HttpRequest | undefined>
|
||||||
|
|
||||||
isDatabaseLoaded(): boolean
|
isDatabaseLoaded(): boolean
|
||||||
onNewDatabaseCreated(): Promise<void>
|
onNewDatabaseCreated(): Promise<void>
|
||||||
loadDatabasePayloads(): Promise<void>
|
loadDatabasePayloads(): Promise<void>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function environmentToString(environment: Environment) {
|
|||||||
[Environment.Web]: 'web',
|
[Environment.Web]: 'web',
|
||||||
[Environment.Desktop]: 'desktop',
|
[Environment.Desktop]: 'desktop',
|
||||||
[Environment.Mobile]: 'native-mobile-web',
|
[Environment.Mobile]: 'native-mobile-web',
|
||||||
|
[Environment.Clipper]: 'clipper',
|
||||||
}
|
}
|
||||||
return map[environment]
|
return map[environment]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,15 +374,8 @@ export class LegacyApiService
|
|||||||
if (preprocessingError) {
|
if (preprocessingError) {
|
||||||
return preprocessingError
|
return preprocessingError
|
||||||
}
|
}
|
||||||
const path = Paths.v1.sync
|
const request = this.getSyncHttpRequest(payloads, lastSyncToken, paginationToken, limit, sharedVaultUuids)
|
||||||
const params = this.params({
|
const response = await this.httpService.runHttp<RawSyncResponse>(request)
|
||||||
[ApiEndpointParam.SyncPayloads]: payloads,
|
|
||||||
[ApiEndpointParam.LastSyncToken]: lastSyncToken,
|
|
||||||
[ApiEndpointParam.PaginationToken]: paginationToken,
|
|
||||||
[ApiEndpointParam.SyncDlLimit]: limit,
|
|
||||||
[ApiEndpointParam.SharedVaultUuids]: sharedVaultUuids,
|
|
||||||
})
|
|
||||||
const response = await this.httpService.post<RawSyncResponse>(path, params, this.getSessionAccessToken())
|
|
||||||
|
|
||||||
if (isErrorResponse(response)) {
|
if (isErrorResponse(response)) {
|
||||||
this.preprocessAuthenticatedErrorResponse(response)
|
this.preprocessAuthenticatedErrorResponse(response)
|
||||||
@@ -394,6 +387,29 @@ export class LegacyApiService
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSyncHttpRequest(
|
||||||
|
payloads: ServerSyncPushContextualPayload[],
|
||||||
|
lastSyncToken: string | undefined,
|
||||||
|
paginationToken: string | undefined,
|
||||||
|
limit: number,
|
||||||
|
sharedVaultUuids?: string[] | undefined,
|
||||||
|
): HttpRequest {
|
||||||
|
const path = Paths.v1.sync
|
||||||
|
const params = this.params({
|
||||||
|
[ApiEndpointParam.SyncPayloads]: payloads,
|
||||||
|
[ApiEndpointParam.LastSyncToken]: lastSyncToken,
|
||||||
|
[ApiEndpointParam.PaginationToken]: paginationToken,
|
||||||
|
[ApiEndpointParam.SyncDlLimit]: limit,
|
||||||
|
[ApiEndpointParam.SharedVaultUuids]: sharedVaultUuids,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
url: joinPaths(this.host, path),
|
||||||
|
params,
|
||||||
|
verb: HttpVerb.Post,
|
||||||
|
authentication: this.getSessionAccessToken(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async refreshSession(): Promise<HttpResponse<SessionRenewalResponse>> {
|
async refreshSession(): Promise<HttpResponse<SessionRenewalResponse>> {
|
||||||
const preprocessingError = this.preprocessingError()
|
const preprocessingError = this.preprocessingError()
|
||||||
if (preprocessingError) {
|
if (preprocessingError) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
|
|
||||||
export const MINIMUM_PASSWORD_LENGTH = 8
|
export const MINIMUM_PASSWORD_LENGTH = 8
|
||||||
export const MissingAccountParams = 'missing-params'
|
export const MissingAccountParams = 'missing-params'
|
||||||
|
const ThirtyMinutes = 30 * 60 * 1000
|
||||||
|
|
||||||
const cleanedEmailString = (email: string) => {
|
const cleanedEmailString = (email: string) => {
|
||||||
return email.trim().toLowerCase()
|
return email.trim().toLowerCase()
|
||||||
@@ -837,4 +838,27 @@ export class SessionManager
|
|||||||
|
|
||||||
return Result.ok(sessionOrError.getValue())
|
return Result.ok(sessionOrError.getValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshSessionIfExpiringSoon(): Promise<boolean> {
|
||||||
|
const session = this.getSession()
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (session instanceof LegacySession) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessTokenExpiration = new Date(session.accessToken.expiresAt)
|
||||||
|
const refreshTokenExpiration = new Date(session.refreshToken.expiresAt)
|
||||||
|
|
||||||
|
const willAccessTokenExpireSoon = accessTokenExpiration.getTime() - Date.now() < ThirtyMinutes
|
||||||
|
const willRefreshTokenExpireSoon = refreshTokenExpiration.getTime() - Date.now() < ThirtyMinutes
|
||||||
|
|
||||||
|
if (willAccessTokenExpireSoon || willRefreshTokenExpireSoon) {
|
||||||
|
return this.httpService.refreshSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConflictParams, ConflictType } from '@standardnotes/responses'
|
import { ConflictParams, ConflictType, HttpRequest } from '@standardnotes/responses'
|
||||||
import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation'
|
import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation'
|
||||||
import {
|
import {
|
||||||
LoggerInterface,
|
LoggerInterface,
|
||||||
@@ -922,6 +922,26 @@ export class SyncService
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRawSyncRequestForExternalUse(
|
||||||
|
items: (DecryptedItemInterface | DeletedItemInterface)[],
|
||||||
|
): Promise<HttpRequest | undefined> {
|
||||||
|
if (this.dealloced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const online = this.sessionManager.online()
|
||||||
|
|
||||||
|
if (!online) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloads = await this.payloadsByPreparingForServer(items.map((i) => i.payloadRepresentation()))
|
||||||
|
const syncToken = await this.getLastSyncToken()
|
||||||
|
const paginationToken = await this.getPaginationToken()
|
||||||
|
|
||||||
|
return this.apiService.getSyncHttpRequest(payloads, syncToken, paginationToken, 150)
|
||||||
|
}
|
||||||
|
|
||||||
private async handleOfflineResponse(response: OfflineSyncResponse) {
|
private async handleOfflineResponse(response: OfflineSyncResponse) {
|
||||||
this.logger.debug('Offline Sync Response', response)
|
this.logger.debug('Offline Sync Response', response)
|
||||||
|
|
||||||
|
|||||||
@@ -1047,4 +1047,25 @@ describe('online syncing', function () {
|
|||||||
|
|
||||||
await contextB.deinit()
|
await contextB.deinit()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should sync note when running raw sync request for external use', async function () {
|
||||||
|
const contextA = this.context
|
||||||
|
const contextB = await Factory.createAppContextWithFakeCrypto('AppB', contextA.email, contextA.password)
|
||||||
|
|
||||||
|
await contextB.launch()
|
||||||
|
await contextB.signIn()
|
||||||
|
|
||||||
|
const notePayload = Factory.createNote()
|
||||||
|
|
||||||
|
const rawSyncRequest = await this.application.sync.getRawSyncRequestForExternalUse([notePayload])
|
||||||
|
expect(rawSyncRequest).to.be.ok
|
||||||
|
|
||||||
|
const response = await this.application.http.runHttp(rawSyncRequest)
|
||||||
|
expect(response.status).to.equal(200)
|
||||||
|
|
||||||
|
await contextB.sync()
|
||||||
|
|
||||||
|
const note = contextB.application.items.findItem(notePayload.uuid)
|
||||||
|
expect(note).to.be.ok
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useApplication } from '../ApplicationProvider'
|
|||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import Menu from '../Menu/Menu'
|
import Menu from '../Menu/Menu'
|
||||||
import MenuItem from '../Menu/MenuItem'
|
import MenuItem from '../Menu/MenuItem'
|
||||||
import { storage as extensionStorage, windows } from 'webextension-polyfill'
|
import { storage as extensionStorage, runtime, windows } from 'webextension-polyfill'
|
||||||
import sendMessageToActiveTab from '@standardnotes/clipper/src/utils/sendMessageToActiveTab'
|
import sendMessageToActiveTab from '@standardnotes/clipper/src/utils/sendMessageToActiveTab'
|
||||||
import { ClipPayload, RuntimeMessageTypes } from '@standardnotes/clipper/src/types/message'
|
import { ClipPayload, RuntimeMessageTypes } from '@standardnotes/clipper/src/types/message'
|
||||||
import { confirmDialog } from '@standardnotes/ui-services'
|
import { confirmDialog } from '@standardnotes/ui-services'
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
PrefKey,
|
PrefKey,
|
||||||
SNNote,
|
SNNote,
|
||||||
SNTag,
|
SNTag,
|
||||||
|
classNames,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { addToast, ToastType } from '@standardnotes/toast'
|
import { addToast, ToastType } from '@standardnotes/toast'
|
||||||
import { getSuperJSONFromClipPayload } from './getSuperJSONFromClipHTML'
|
import { getSuperJSONFromClipPayload } from './getSuperJSONFromClipHTML'
|
||||||
@@ -36,6 +37,7 @@ import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdow
|
|||||||
import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
||||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
|
import Spinner from '../Spinner/Spinner'
|
||||||
|
|
||||||
const ClipperView = ({
|
const ClipperView = ({
|
||||||
viewControllerManager,
|
viewControllerManager,
|
||||||
@@ -60,6 +62,11 @@ const ClipperView = ({
|
|||||||
const isFirefoxPopup = !!currentWindow && currentWindow.type === 'popup' && currentWindow.incognito === false
|
const isFirefoxPopup = !!currentWindow && currentWindow.type === 'popup' && currentWindow.incognito === false
|
||||||
|
|
||||||
const [user, setUser] = useState(() => application.getUser())
|
const [user, setUser] = useState(() => application.getUser())
|
||||||
|
const [isSyncing, setIsSyncing] = useState(false)
|
||||||
|
const [hasSyncError, setHasSyncError] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
application.sessions.refreshSessionIfExpiringSoon().catch(console.error)
|
||||||
|
}, [application.sessions])
|
||||||
const [isEntitledToExtension, setIsEntitled] = useState(
|
const [isEntitledToExtension, setIsEntitled] = useState(
|
||||||
() =>
|
() =>
|
||||||
application.features.getFeatureStatus(
|
application.features.getFeatureStatus(
|
||||||
@@ -88,6 +95,13 @@ const ClipperView = ({
|
|||||||
) === FeatureStatus.Entitled,
|
) === FeatureStatus.Entitled,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case ApplicationEvent.SyncStatusChanged:
|
||||||
|
case ApplicationEvent.FailedSync: {
|
||||||
|
const status = application.sync.getSyncStatus()
|
||||||
|
setIsSyncing(status.syncInProgress)
|
||||||
|
setHasSyncError(status.hasError())
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [application])
|
}, [application])
|
||||||
@@ -241,10 +255,19 @@ const ClipperView = ({
|
|||||||
message: 'Note clipped successfully',
|
message: 'Note clipped successfully',
|
||||||
})
|
})
|
||||||
|
|
||||||
void application.sync.sync()
|
const syncRequest = await application.sync.getRawSyncRequestForExternalUse([insertedNote])
|
||||||
|
|
||||||
|
if (syncRequest) {
|
||||||
|
runtime
|
||||||
|
.sendMessage({
|
||||||
|
type: RuntimeMessageTypes.RunHttpRequest,
|
||||||
|
payload: syncRequest,
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void createNoteFromClip()
|
createNoteFromClip().catch(console.error)
|
||||||
}, [
|
}, [
|
||||||
application.items,
|
application.items,
|
||||||
application.linkingController,
|
application.linkingController,
|
||||||
@@ -431,6 +454,22 @@ const ClipperView = ({
|
|||||||
<Icon type="signOut" className="text-neutral" />
|
<Icon type="signOut" className="text-neutral" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{isSyncing || hasSyncError ? (
|
||||||
|
<div className={classNames('flex items-center border-t border-border', hasSyncError && 'text-danger')}>
|
||||||
|
{isSyncing && (
|
||||||
|
<>
|
||||||
|
<Spinner className="w-4 h-4 mx-2.5" />
|
||||||
|
<div className="flex-grow py-2 text-sm font-semibold text-info">Syncing...</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{hasSyncError && (
|
||||||
|
<>
|
||||||
|
<Icon type="warning" className="mx-2.5" />
|
||||||
|
<div className="flex-grow py-2 text-sm font-semibold">Unable to sync</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user