Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 19 additions & 120 deletions src/LiveViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,65 +124,24 @@ export class LoggedOutView implements S3View {
this.login = login;
}

setLoginIcon(): void {
const viewHeaderElement =
this.view.containerEl.querySelector(".view-header");
const viewHeaderLeftElement =
this.view.containerEl.querySelector(".view-header-left");

if (viewHeaderElement && viewHeaderLeftElement) {
this.clearLoginButton();

// Create login button element
const loginButton = document.createElement("button");
loginButton.className = "view-header-left system3-login-button";
loginButton.textContent = "Login to enable Live edits";
loginButton.setAttribute("aria-label", "Login to enable Live edits");
loginButton.setAttribute("tabindex", "0");

// Add click handler
loginButton.addEventListener("click", async () => {
await this.login();
});

// Insert after view-header-left
viewHeaderLeftElement.insertAdjacentElement("afterend", loginButton);
}
}

clearLoginButton() {
const existingButton = this.view.containerEl.querySelector(".system3-login-button");
if (existingButton) {
existingButton.remove();
}
}

attach(): Promise<S3View> {
// Use header button approach on mobile for Obsidian >=1.11.0 to avoid banner positioning issues
if (Platform.isMobile && requireApiVersion("1.11.0")) {
this.setLoginIcon();
} else {
this.banner = new Banner(
this.view,
"Login to enable Live edits",
async () => {
return await this.login();
},
);
}
this.banner = new Banner(
this.view,
{ short: "Login to Relay", long: "Login to enable Live edits" },
async () => {
return await this.login();
},
);
return Promise.resolve(this);
}

release() {
this.banner?.destroy();
this.clearLoginButton();
}

destroy() {
this.release();
this.banner?.destroy();
this.banner = undefined;
this.clearLoginButton();
this.view = null as any;
}
}
Expand Down Expand Up @@ -260,7 +219,7 @@ export class RelayCanvasView implements S3View {
if (this.shouldConnect) {
const banner = new Banner(
this.view,
"You're offline -- click to reconnect",
{ short: "Offline", long: "You're offline -- click to reconnect" },
async () => {
this._parent.networkStatus.checkStatus();
this.connect();
Expand Down Expand Up @@ -475,37 +434,23 @@ export class LiveView<ViewType extends TextFileView>
}
}

setMergeButton(): void {
const viewHeaderElement =
this.view.containerEl.querySelector(".view-header");
const viewHeaderLeftElement =
this.view.containerEl.querySelector(".view-header-left");

if (viewHeaderElement && viewHeaderLeftElement) {
this.clearMergeButton();

// Create merge button element
const mergeButton = document.createElement("button");
mergeButton.className = "view-header-left system3-merge-button";
mergeButton.textContent = "Merge conflict";
mergeButton.setAttribute("aria-label", "Merge conflict -- click to resolve");
mergeButton.setAttribute("tabindex", "0");

// Add click handler
mergeButton.addEventListener("click", async () => {
mergeBanner(): () => void {
this._banner = new Banner(
this.view,
{ short: "Merge conflict", long: "Merge conflict -- click to resolve" },
async () => {
const diskBuffer = await this.document.diskBuffer();
const stale = await this.document.checkStale();
if (!stale) {
this.clearMergeButton();
return;
return true;
}
this._parent.openDiffView({
file1: this.document,
file2: diskBuffer,
showMergeOption: true,
onResolve: async () => {
this.document.clearDiskBuffer();
this.clearMergeButton();
this._banner?.destroy();
// Force view to sync to CRDT state after differ resolution
if (
this._plugin &&
Expand All @@ -515,61 +460,17 @@ export class LiveView<ViewType extends TextFileView>
}
},
});
});

// Insert after view-header-left
viewHeaderLeftElement.insertAdjacentElement("afterend", mergeButton);
}
}

clearMergeButton() {
const existingButton = this.view.containerEl.querySelector(".system3-merge-button");
if (existingButton) {
existingButton.remove();
}
}

mergeBanner(): () => void {
// Use header button approach on mobile for Obsidian >=1.11.0 to avoid banner positioning issues
if (Platform.isMobile && requireApiVersion("1.11.0")) {
this.setMergeButton();
} else {
this._banner = new Banner(
this.view,
"Merge conflict -- click to resolve",
async () => {
const diskBuffer = await this.document.diskBuffer();
const stale = await this.document.checkStale();
if (!stale) {
return true;
}
this._parent.openDiffView({
file1: this.document,
file2: diskBuffer,
showMergeOption: true,
onResolve: async () => {
this.document.clearDiskBuffer();
// Force view to sync to CRDT state after differ resolution
if (
this._plugin &&
typeof this._plugin.syncViewToCRDT === "function"
) {
await this._plugin.syncViewToCRDT();
}
},
});
return true;
},
);
}
return true;
},
);
return () => {};
}

offlineBanner(): () => void {
if (this.shouldConnect) {
const banner = new Banner(
this.view,
"You're offline -- click to reconnect",
{ short: "Offline", long: "You're offline -- click to reconnect" },
async () => {
this._parent.networkStatus.checkStatus();
this.connect();
Expand Down Expand Up @@ -725,7 +626,6 @@ export class LiveView<ViewType extends TextFileView>
this._viewActions = undefined;
this._banner?.destroy();
this._banner = undefined;
this.clearMergeButton();
if (this.offConnectionStatusSubscription) {
this.offConnectionStatusSubscription();
this.offConnectionStatusSubscription = undefined;
Expand All @@ -741,7 +641,6 @@ export class LiveView<ViewType extends TextFileView>
destroy() {
this.release();
this.clearViewActions();
this.clearMergeButton();
(this.view.leaf as any).rebuildView?.();
this._parent = null as any;
this.view = null as any;
Expand Down
68 changes: 60 additions & 8 deletions src/ui/Banner.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
"use strict";
import { TextFileView } from "obsidian";
import { Platform, requireApiVersion, TextFileView } from "obsidian";
import type { CanvasView } from "src/CanvasView";

export type BannerText = string | { short: string; long: string };

export class Banner {
view: TextFileView | CanvasView;
text: string;
text: BannerText;
onClick: () => Promise<boolean>;
private useHeaderButton: boolean;

constructor(
view: TextFileView | CanvasView,
text: string,
text: BannerText,
onClick: () => Promise<boolean>,
) {
this.view = view;
this.text = text;
this.onClick = onClick;
// Use header button approach on mobile for Obsidian >=1.11.0 to avoid banner positioning issues
this.useHeaderButton = Platform.isMobile && requireApiVersion("1.11.0");
this.display();
}

private get shortText(): string {
return typeof this.text === "string" ? this.text : this.text.short;
}

private get longText(): string {
return typeof this.text === "string" ? this.text : this.text.long;
}

display() {
if (!this.view) return true;
const leafContentEl = this.view.containerEl;
const contentEl = this.view.containerEl.querySelector(".view-content");

if (!leafContentEl) {
return;
}

if (this.useHeaderButton) {
return this.displayHeaderButton();
}

const contentEl = this.view.containerEl.querySelector(".view-content");

// container to enable easy removal of the banner
let bannerBox = leafContentEl.querySelector(".system3-banner-box");
if (!bannerBox) {
Expand All @@ -40,7 +58,7 @@ export class Banner {
banner = document.createElement("div");
banner.classList.add("system3-banner");
const span = banner.createSpan();
span.setText(this.text);
span.setText(this.longText);
banner.appendChild(span);
bannerBox.appendChild(banner);
const onClick = async () => {
Expand All @@ -54,14 +72,48 @@ export class Banner {
return true;
}

private displayHeaderButton() {
const leafContentEl = this.view.containerEl;
const viewHeaderLeftElement =
leafContentEl.querySelector(".view-header-left");

if (!viewHeaderLeftElement) {
return;
}

// Remove existing button if any
leafContentEl.querySelector(".system3-header-button")?.remove();

const button = document.createElement("button");
button.className = "view-header-left system3-header-button";
button.textContent = this.shortText;
button.setAttribute("aria-label", this.longText);
button.setAttribute("tabindex", "0");

button.addEventListener("click", async () => {
const destroy = await this.onClick();
if (destroy) {
this.destroy();
}
});

viewHeaderLeftElement.insertAdjacentElement("afterend", button);
return true;
}

destroy() {
const leafContentEl = this.view.containerEl;
if (!leafContentEl) {
return;
}
const bannerBox = leafContentEl.querySelector(".system3-banner-box");
if (bannerBox) {
bannerBox.replaceChildren();

if (this.useHeaderButton) {
leafContentEl.querySelector(".system3-header-button")?.remove();
} else {
const bannerBox = leafContentEl.querySelector(".system3-banner-box");
if (bannerBox) {
bannerBox.replaceChildren();
}
}
this.onClick = async () => true;
return true;
Expand Down
6 changes: 2 additions & 4 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,15 @@
}

.system3-banner > span {
font-weight: bold;
display: flex;
flex-grow: 1;
font-size: var(--font-ui-medium);
color: var(--text-on-accent);
font: var(--font-interface-theme);
text-shadow: var(--input-shadow);
}

.system3-login-button,
.system3-merge-button {
.system3-header-button,
.system3-login-button {
background: var(--interactive-accent) !important;
color: var(--text-on-accent) !important;
border: var(--modal-border-width) solid var(--pill-border-color-hover) !important;
Expand Down