Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/channel-enabled-by-default.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@plotday/twister": minor
---

Added: `enabledByDefault?: boolean` (tri-state) on the `Channel` type returned by `getChannels()`. Lets a connector influence which channels are pre-selected when a connection is first added: `true` pre-selects, `false` excludes a low-value/irrelevant channel or a too-broad container (e.g. a holiday or someone-else's shared calendar, a GitHub org that cascades to every repo, a Teams team container), and `undefined` leaves the choice to the client (which enables the channel unless its title looks low-value). The setup UI now defaults to enabling all top-level channels and filtering out the low-value ones, rather than picking a single "primary."
4 changes: 4 additions & 0 deletions connectors/github/src/github.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -337,6 +337,10 @@ export class GitHub extends Connector<GitHub> {
channels.push({
id: owner,
title: owner,
// Enabling an owner cascades to EVERY repo under it (orgs can have
// hundreds), so don't pre-select it. The user opts in to the specific
// owner or repos they want to sync.
enabledByDefault: false,
children: ownerRepos.map((repo) => ({
id: repo.full_name,
title: repo.full_name,
Expand Down
12 changes: 11 additions & 1 deletion connectors/gmail/src/gmail.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -400,7 +400,17 @@ export class Gmail extends Connector<Gmail> {
l.type !== "system" ||
["INBOX", "SENT", "DRAFT", "IMPORTANT", "STARRED"].includes(l.id)
)
.map((l: any) => ({ id: l.id, title: l.name }));
// Default to syncing the user's actual conversations: Inbox (incoming)
// and Sent (outgoing). Important/Starred are overlapping views of mail
// that's mostly already in the Inbox, so enabling them by default would
// largely re-sync the same threads; Draft and user-created labels would
// crowd the view. They're all still available to enable manually, and
// Spam/Trash aren't even listed (filtered above).
.map((l: any) => ({
id: l.id,
title: l.name,
enabledByDefault: l.id === "INBOX" || l.id === "SENT",
}));
}

async onChannelEnabled(channel: Channel, context?: SyncContext): Promise<void> {
Expand Down
24 changes: 23 additions & 1 deletion connectors/google-calendar/src/google-calendar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,6 +44,14 @@ type Calendar = {
name: string;
description: string | null;
primary: boolean;
/**
* The user's access level on this calendar: "owner", "writer", "reader",
* or "freeBusyReader". Calendars the user owns (their primary + any
* secondary calendars they created) are "owner"; subscribed holiday/
* birthday calendars and someone-else's shared calendars are "reader"/
* "writer". Drives the default-enable decision in getChannels.
*/
accessRole: string | null;
};

/**
Expand DownExpand Up@@ -203,7 +211,17 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
): Promise<Channel[]> {
const api = new GoogleApi(token.token);
const calendars = await this.listCalendarsWithApi(api);
return calendars.map((c) => ({ id: c.id, title: c.name }));
// Default to syncing the user's OWN calendars (their primary + any
// secondary calendars they created — accessRole "owner"). Calendars the
// user merely subscribes to (holidays, birthdays) or that are shared with
// them by someone else are "reader"/"writer"; exclude those from the
// default selection so they don't crowd the user's view. The user can
// still enable any of them manually.
return calendars.map((c) => ({
id: c.id,
title: c.name,
enabledByDefault: c.accessRole === "owner",
}));
}

/**
Expand DownExpand Up@@ -372,13 +390,15 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

return data.items.map((item) => ({
id: item.id,
name: item.summary,
description: item.description || null,
accessRole: item.accessRole ?? null,
primary: item.primary || false,
}));
}
Expand DownExpand Up@@ -440,6 +460,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
summary: string;
description?: string;
primary?: boolean;
accessRole?: string;
}>;
};

Expand All@@ -448,6 +469,7 @@ export class GoogleCalendar extends Connector<GoogleCalendar> {
name: item.summary,
description: item.description || null,
primary: item.primary || false,
accessRole: item.accessRole ?? null,
}));
}

Expand Down
4 changes: 4 additions & 0 deletions connectors/google-drive/src/google-drive.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,6 +275,10 @@ export class GoogleDrive extends Connector<GoogleDrive> {
},
];

// My Drive, Shared drives, and Shared with me are all enabled by default
// (the client default for top-level channels) — a Drive user generally
// wants their documents from all three. Individual sub-folders stay
// collapsed under each, so the user can still narrow the scope.
if (sharedDriveMap.size > 0) {
result.push({
id: VIRTUAL_SHARED_DRIVES,
Expand Down
3 changes: 3 additions & 0 deletions connectors/ms-teams/src/ms-teams.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -157,6 +157,9 @@ export class MsTeams extends Connector<MsTeams> {
channels.push({
id: team.id,
title: team.displayName,
// A team is a container, not a syncable resource — the user picks the
// specific channels under it. Don't pre-select the team itself.
enabledByDefault: false,
children: teamChannels.map((ch) => ({
id: ch.id,
title: ch.displayName,
Expand Down
20 changes: 20 additions & 0 deletions twister/src/tools/integrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,26 @@ export type Channel = {
id: string;
/** Display name shown in the UI */
title: string;
/**
* Whether this channel should be selected by default when the user first
* adds the connection. Tri-state:
* - `true` — pre-select it (e.g. the user's own/primary calendar).
* - `false` — exclude it from the default selection (low-value or
* irrelevant resources that would crowd the user's view, or containers
* whose contents are too broad to sync wholesale — e.g. a holiday or
* someone-else's shared calendar, a GitHub org that cascades to every
* repo, a Microsoft Teams team container). The user can still enable it
* manually.
* - `undefined` — no opinion; the client decides. The client defaults to
* enabling the channel unless its title looks low-value (holidays,
* birthdays, spam/sent/draft, …).
*
* The guiding principle is "sync everything the user would reasonably want
* by default" — for most connectors that's all channels, so only set this
* where the connector can distinguish the user's own/relevant channels from
* low-value ones (e.g. Google Calendar via `accessRole === "owner"`).
*/
enabledByDefault?: boolean;
/** Optional nested channel resources (e.g., subfolders) */
children?: Channel[];
/** Per-channel link type configs. Overrides twist-level linkTypes when present. */
Expand Down
Loading