Skip to content

Commit 30034ec

Browse files
juliusmarmingeJulius Marmingecodex
authored
Add archived threads and mobile file viewer (#3155)
Co-authored-by: Julius Marminge <julius@mac.lan> Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 1fcc57a commit 30034ec

93 files changed

Lines changed: 6791 additions & 1136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src/preview/Manager.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,58 @@ describe("PreviewManager", () => {
128128
),
129129
);
130130

131+
effectIt.effect("queues navigation until the webview registers", () =>
132+
withManager((manager) =>
133+
Effect.gen(function* () {
134+
const loadURL = vi.fn(async () => undefined);
135+
const listeners = new Map<string, (...args: never[]) => void>();
136+
fromId.mockReturnValue({
137+
id: 42,
138+
isDestroyed: () => false,
139+
getType: () => "webview",
140+
getURL: () => "about:blank",
141+
getTitle: () => "",
142+
isLoading: () => false,
143+
getZoomFactor: () => 1,
144+
setZoomFactor: vi.fn(),
145+
loadURL,
146+
on: vi.fn((event: string, listener: (...args: never[]) => void) => {
147+
listeners.set(event, listener);
148+
}),
149+
off: vi.fn(),
150+
ipc: { on: vi.fn(), off: vi.fn() },
151+
send: webviewSend,
152+
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
153+
setWindowOpenHandler: vi.fn(),
154+
debugger: {
155+
isAttached: () => false,
156+
attach: vi.fn(),
157+
sendCommand: vi.fn(async () => undefined),
158+
on: vi.fn(),
159+
off: vi.fn(),
160+
},
161+
} as never);
162+
163+
yield* manager.navigate("tab_pending", "localhost:3200");
164+
165+
expect(yield* manager.automationStatus("tab_pending")).toEqual({
166+
available: false,
167+
visible: true,
168+
tabId: "tab_pending",
169+
url: "http://localhost:3200/",
170+
title: "",
171+
loading: true,
172+
});
173+
174+
yield* manager.registerWebview("tab_pending", 42);
175+
yield* Effect.yieldNow;
176+
177+
expect(loadURL).toHaveBeenCalledOnce();
178+
expect(loadURL).toHaveBeenCalledWith("http://localhost:3200/");
179+
}),
180+
),
181+
);
182+
131183
effectIt.effect("captures a PNG screenshot into browser artifacts", () =>
132184
withManager((manager) =>
133185
Effect.gen(function* () {

apps/desktop/src/preview/Manager.ts

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,26 +1195,103 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
11951195
}
11961196
yield* attachListeners(tabId, wc);
11971197
runFork(ensureControlSession(wc).pipe(Effect.ignore));
1198-
if (Math.abs(tab.zoomFactor - DEFAULT_ZOOM_FACTOR) > ZOOM_EPSILON) {
1199-
yield* attempt("registerWebview.restoreZoom", () => wc.setZoomFactor(tab.zoomFactor)).pipe(
1200-
Effect.ignore,
1201-
);
1202-
}
1203-
yield* update(tabId, {
1204-
webContentsId,
1205-
navStatus: computeNavStatus(wc),
1206-
canGoBack: wc.navigationHistory.canGoBack(),
1207-
canGoForward: wc.navigationHistory.canGoForward(),
1208-
zoomFactor: tab.zoomFactor,
1198+
const registeredAt = yield* currentIso;
1199+
const registration = yield* SynchronizedRef.modify(tabsRef, (tabs) => {
1200+
const current = tabs.get(tabId);
1201+
if (!current) {
1202+
return [
1203+
Option.none<{ readonly state: PreviewTabState; readonly pendingUrl: string | null }>(),
1204+
tabs,
1205+
] as const;
1206+
}
1207+
const pendingUrl = current.navStatus.kind === "Loading" ? current.navStatus.url : null;
1208+
const next: PreviewTabState = {
1209+
...current,
1210+
webContentsId,
1211+
navStatus: pendingUrl === null ? computeNavStatus(wc) : current.navStatus,
1212+
canGoBack: wc.navigationHistory.canGoBack(),
1213+
canGoForward: wc.navigationHistory.canGoForward(),
1214+
updatedAt: registeredAt,
1215+
};
1216+
return [
1217+
Option.some({
1218+
state: next,
1219+
pendingUrl,
1220+
}),
1221+
replaceMap(tabs, (copy) => {
1222+
copy.set(tabId, next);
1223+
}),
1224+
] as const;
12091225
});
1226+
if (Option.isNone(registration)) {
1227+
return yield* fail("registerWebview", new PreviewTabNotFoundError(tabId));
1228+
}
1229+
const { state: registered, pendingUrl } = registration.value;
1230+
yield* emit(tabId, registered);
1231+
if (Math.abs(registered.zoomFactor - DEFAULT_ZOOM_FACTOR) > ZOOM_EPSILON) {
1232+
yield* attempt("registerWebview.restoreZoom", () =>
1233+
wc.setZoomFactor(registered.zoomFactor),
1234+
).pipe(Effect.ignore);
1235+
}
12101236
yield* attempt("registerWebview.sendTheme", () =>
12111237
wc.send(ANNOTATION_THEME_CHANNEL, annotationTheme),
12121238
);
1239+
const latestNavStatus = (yield* SynchronizedRef.get(tabsRef)).get(tabId)?.navStatus;
1240+
if (
1241+
pendingUrl &&
1242+
latestNavStatus?.kind === "Loading" &&
1243+
latestNavStatus.url === pendingUrl &&
1244+
wc.getURL() !== pendingUrl
1245+
) {
1246+
runFork(
1247+
attemptPromise("registerWebview.loadPendingUrl", () => wc.loadURL(pendingUrl)).pipe(
1248+
Effect.ignore,
1249+
),
1250+
);
1251+
}
12131252
});
12141253

12151254
const navigate = Effect.fn("PreviewManager.navigate")(function* (tabId: string, rawUrl: string) {
1216-
const wc = yield* requireWebContents(tabId);
12171255
const url = yield* attempt("navigate.normalizeUrl", () => normalizePreviewUrl(rawUrl));
1256+
const updatedAt = yield* currentIso;
1257+
const pending = yield* SynchronizedRef.modify(tabsRef, (tabs) => {
1258+
const current = tabs.get(tabId);
1259+
const next: PreviewTabState = {
1260+
tabId,
1261+
webContentsId: current?.webContentsId ?? null,
1262+
navStatus: {
1263+
kind: "Loading",
1264+
url,
1265+
title: current?.navStatus.kind === "Idle" || !current ? "" : current.navStatus.title,
1266+
},
1267+
canGoBack: current?.canGoBack ?? false,
1268+
canGoForward: current?.canGoForward ?? false,
1269+
zoomFactor: current?.zoomFactor ?? DEFAULT_ZOOM_FACTOR,
1270+
controller: current?.controller ?? "none",
1271+
updatedAt,
1272+
};
1273+
return [
1274+
next,
1275+
replaceMap(tabs, (copy) => {
1276+
copy.set(tabId, next);
1277+
}),
1278+
] as const;
1279+
});
1280+
yield* emit(tabId, pending);
1281+
if (pending.webContentsId == null) return;
1282+
const wc = webContents.fromId(pending.webContentsId);
1283+
if (!wc) {
1284+
const detached = { ...pending, webContentsId: null };
1285+
yield* SynchronizedRef.update(tabsRef, (tabs) =>
1286+
tabs.get(tabId)?.webContentsId !== pending.webContentsId
1287+
? tabs
1288+
: replaceMap(tabs, (copy) => {
1289+
copy.set(tabId, detached);
1290+
}),
1291+
);
1292+
yield* emit(tabId, detached);
1293+
return;
1294+
}
12181295
if (wc.getURL() === url) {
12191296
yield* attempt("navigate.reload", () => wc.reload());
12201297
return;

apps/mobile/modules/t3-markdown-text/ios/T3MarkdownText.mm

Lines changed: 8 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ static void T3MarkdownTextApplyAttachments(
7070
renderingMode:UIImageRenderingModeAlwaysOriginal];
7171
}
7272
attachment.image = image ?: [[UIImage alloc] init];
73-
attachment.bounds = CGRectMake(0, -0.5, 10, 10);
73+
const CGFloat attachmentSize = T3MarkdownTextAttachmentSize(attachmentRange);
74+
attachment.bounds = CGRectMake(
75+
0,
76+
T3MarkdownTextAttachmentBaselineOffset(attachmentRange),
77+
attachmentSize,
78+
attachmentSize);
7479
const NSRange range = NSMakeRange(
7580
attachmentRange.location,
7681
MIN(attachmentRange.length, attributedString.length - attachmentRange.location));
@@ -80,104 +85,6 @@ static void T3MarkdownTextApplyAttachments(
8085
}
8186
}
8287

83-
static NSArray<NSDictionary<NSString *, id> *> *T3MarkdownTextExtractChipBackgrounds(
84-
NSMutableAttributedString *attributedString,
85-
const std::vector<T3MarkdownTextChipRange> &chipRanges)
86-
{
87-
NSMutableArray<NSDictionary<NSString *, id> *> *backgrounds = [NSMutableArray array];
88-
for (const auto &chipRange : chipRanges) {
89-
if (chipRange.length == 0 || chipRange.location >= attributedString.length) {
90-
continue;
91-
}
92-
93-
const NSRange range = NSMakeRange(
94-
chipRange.location,
95-
MIN(chipRange.length, attributedString.length - chipRange.location));
96-
UIColor *color = [attributedString attribute:NSBackgroundColorAttributeName
97-
atIndex:range.location
98-
effectiveRange:nil];
99-
UIColor *foregroundColor = [attributedString attribute:NSForegroundColorAttributeName
100-
atIndex:range.location
101-
effectiveRange:nil];
102-
if (color == nil) {
103-
continue;
104-
}
105-
[backgrounds addObject:@{
106-
@"range": [NSValue valueWithRange:range],
107-
@"color": color,
108-
@"strokeColor": [foregroundColor
109-
colorWithAlphaComponent:chipRange.isSkill ? 0.25 : 0.1] ?: UIColor.clearColor,
110-
}];
111-
[attributedString removeAttribute:NSBackgroundColorAttributeName range:range];
112-
}
113-
return backgrounds;
114-
}
115-
116-
@interface T3MarkdownTextBackingView : UITextView
117-
@property(nonatomic, copy) NSArray<NSDictionary<NSString *, id> *> *chipBackgrounds;
118-
@end
119-
120-
@implementation T3MarkdownTextBackingView
121-
122-
- (void)drawRect:(CGRect)rect
123-
{
124-
[self.layoutManager ensureLayoutForTextContainer:self.textContainer];
125-
CGContextRef context = UIGraphicsGetCurrentContext();
126-
if (context != nil) {
127-
CGContextSaveGState(context);
128-
CGContextResetClip(context);
129-
CGContextClipToRect(context, self.bounds);
130-
}
131-
for (NSDictionary<NSString *, id> *background in self.chipBackgrounds) {
132-
const NSRange characterRange = [background[@"range"] rangeValue];
133-
UIColor *color = background[@"color"];
134-
UIColor *strokeColor = background[@"strokeColor"];
135-
if (characterRange.length == 0 || NSMaxRange(characterRange) > self.textStorage.length) {
136-
continue;
137-
}
138-
139-
const NSRange glyphRange =
140-
[self.layoutManager glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
141-
[color setFill];
142-
[self.layoutManager
143-
enumerateEnclosingRectsForGlyphRange:glyphRange
144-
withinSelectedGlyphRange:NSMakeRange(NSNotFound, 0)
145-
inTextContainer:self.textContainer
146-
usingBlock:^(CGRect glyphRect, BOOL *stop) {
147-
const CGFloat chipHeight = 22;
148-
CGRect chipRect = CGRectMake(
149-
glyphRect.origin.x - 4,
150-
CGRectGetMidY(glyphRect) - chipHeight / 2,
151-
glyphRect.size.width + 8,
152-
chipHeight);
153-
chipRect.origin.x += self.textContainerInset.left;
154-
chipRect.origin.y += self.textContainerInset.top;
155-
const CGFloat minimumX = self.textContainerInset.left + 0.5;
156-
const CGFloat maximumX =
157-
CGRectGetWidth(self.bounds) - self.textContainerInset.right - 0.5;
158-
if (chipRect.origin.x < minimumX) {
159-
chipRect.size.width -= minimumX - chipRect.origin.x;
160-
chipRect.origin.x = minimumX;
161-
}
162-
if (CGRectGetMaxX(chipRect) > maximumX) {
163-
chipRect.size.width = MAX(0, maximumX - chipRect.origin.x);
164-
}
165-
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:chipRect cornerRadius:6];
166-
[path fill];
167-
[strokeColor setStroke];
168-
path.lineWidth = 1;
169-
[path stroke];
170-
}];
171-
}
172-
if (context != nil) {
173-
CGContextRestoreGState(context);
174-
}
175-
176-
[super drawRect:rect];
177-
}
178-
179-
@end
180-
18188
@protocol T3MarkdownOutsideTapTarget <NSObject>
18289
- (void)clearSelectionForOutsideTapWithHitView:(UIView *)hitView;
18390
@end
@@ -285,7 +192,7 @@ @interface T3MarkdownText () <T3MarkdownOutsideTapTarget>
285192

286193
@implementation T3MarkdownText {
287194
UIView * _view;
288-
T3MarkdownTextBackingView * _textView;
195+
UITextView * _textView;
289196
T3MarkdownTextShadowNode::ConcreteState::Shared _state;
290197
__weak UIWindow * _outsideTapWindow;
291198
BOOL _suppressSelectionChange;
@@ -308,7 +215,7 @@ - (instancetype)initWithFrame:(CGRect)frame
308215
self.contentView = _view;
309216
self.clipsToBounds = true;
310217

311-
_textView = [[T3MarkdownTextBackingView alloc] init];
218+
_textView = [[UITextView alloc] init];
312219
_attachmentImages = [[NSMutableDictionary alloc] init];
313220
_pendingAttachmentUris = [[NSMutableSet alloc] init];
314221
_textView.scrollEnabled = false;
@@ -405,9 +312,6 @@ - (void)drawRect:(CGRect)rect
405312
convertedAttrString,
406313
_state->getData().attachmentRanges,
407314
_attachmentImages);
408-
_textView.chipBackgrounds = T3MarkdownTextExtractChipBackgrounds(
409-
convertedAttrString,
410-
_state->getData().chipRanges);
411315
[self loadAttachmentImages:_state->getData().attachmentRanges];
412316

413317
// Setting attributedText clears any active text selection, and re-assigning

apps/mobile/modules/t3-markdown-text/ios/T3MarkdownTextShadowNode.h

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,20 @@ struct T3MarkdownTextAttachmentRange {
2828
std::string imageUri;
2929
};
3030

31-
struct T3MarkdownTextChipRange {
32-
size_t location;
33-
size_t length;
34-
bool isSkill;
35-
};
31+
inline Float T3MarkdownTextAttachmentSize(const T3MarkdownTextAttachmentRange &) {
32+
return 14;
33+
}
34+
35+
inline Float T3MarkdownTextAttachmentBaselineOffset(
36+
const T3MarkdownTextAttachmentRange &) {
37+
return -2;
38+
}
3639

3740
class T3MarkdownTextStateReal final {
3841
public:
3942
AttributedString attributedString;
4043
std::vector<T3MarkdownTextParagraphStyleRange> paragraphStyleRanges;
4144
std::vector<T3MarkdownTextAttachmentRange> attachmentRanges;
42-
std::vector<T3MarkdownTextChipRange> chipRanges;
4345
};
4446

4547
class T3MarkdownTextShadowNode final : public ConcreteViewShadowNode<
@@ -72,6 +74,5 @@ T3MarkdownTextStateReal> {
7274
mutable AttributedString _attributedString;
7375
mutable std::vector<T3MarkdownTextParagraphStyleRange> _paragraphStyleRanges;
7476
mutable std::vector<T3MarkdownTextAttachmentRange> _attachmentRanges;
75-
mutable std::vector<T3MarkdownTextChipRange> _chipRanges;
7677
};
7778
} // namespace facebook::React

0 commit comments

Comments
 (0)