diff --git a/package.json b/package.json index af6061c6..5573c6ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.10.24", + "version": "0.11.0", "type": "module", "scripts": { "start": "rm -rf build && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index 9c352fb6..60718331 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -40,6 +40,7 @@ html { --pink: #df4074; --red: #d81316; --red-muted: #ff4949; + --yellow: #e6a817; --muted-gray-light: #666; --muted-gray-dark: #999; --disabled-gray: #464646; @@ -90,6 +91,7 @@ html { --text-link-color: var(--highlight-color); --like-color: var(--pink); --error-color: var(--red-muted); + --warning-color: var(--yellow); /* border colors */ --header-border-color: var(--generic-border-color); @@ -5949,6 +5951,71 @@ moderation-warning .toggle-content { margin-top: 8px; } +#login-form .select-wrapper { + position: relative; + width: 100%; +} + +#login-form .select-wrapper::after { + content: ""; + position: absolute; + right: 14px; + top: 50%; + width: 12px; + height: 8px; + transform: translateY(-50%); + pointer-events: none; + background-color: var(--text-color-muted); + -webkit-mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") + no-repeat center / contain; + mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E") + no-repeat center / contain; +} + +#login-form select { + font-size: 16px; + width: 100%; + padding: 12px 40px 12px 12px; + border-radius: var(--text-input-border-radius); + border: var(--hair) solid var(--post-border-color); + background-color: var(--text-input-background-color); + color: var(--text-color); + appearance: none; + -webkit-appearance: none; + cursor: pointer; +} + +@media (hover: hover) and (pointer: fine) { + #login-form select:hover { + border-color: var(--text-color-muted); + } +} + +#login-advanced { + width: 100%; +} + +#login-advanced summary { + cursor: pointer; + padding: 4px 0; + user-select: none; + color: var(--text-color-muted); +} + +#login-advanced .form-group { + margin-top: 8px; +} + +.warning-area { + margin-top: 12px; + padding: 12px; + background-color: color-mix(in srgb, var(--warning-color) 12%, transparent); + color: var(--text-color); + font-size: 14px; + line-height: 1.5; + border-radius: var(--text-input-border-radius); +} + @media (min-width: 800px) { #login-form .form-title { display: none; diff --git a/src/index.html b/src/index.html index ab895da7..de3ffa50 100644 --- a/src/index.html +++ b/src/index.html @@ -62,6 +62,10 @@ IN_APP_LINK_DOMAINS, } from "/js/config.js"; import { setUpIdentityPrecaching } from "/js/identityPrecaching.js"; + import { + getAppViewConfig, + handleAppViewResetQueryParam, + } from "/js/appViewConfig.js"; // Dev tools import "/js/perf.js"; @@ -76,9 +80,15 @@ document.body.classList.add("native"); } + handleAppViewResetQueryParam(); + const auth = await getAuth(); const session = await auth.getSession(); - const api = new Api(session ?? null); + const appViewConfig = getAppViewConfig(); + const api = new Api(session ?? null, { + bskyAppViewServiceDid: appViewConfig.appViewServiceDid, + chatAppViewServiceDid: appViewConfig.chatServiceDid, + }); const dataLayer = new DataLayer(api); // put dataLayer on window for easy access in dev tools window.dataLayer = dataLayer; diff --git a/src/js/appViewConfig.js b/src/js/appViewConfig.js new file mode 100644 index 00000000..d2c0d60e --- /dev/null +++ b/src/js/appViewConfig.js @@ -0,0 +1,78 @@ +import { AppViewConfig, DEFAULT_APP_VIEW_CONFIGS } from "/js/config.js"; + +const STORAGE_KEY = "appview-config"; +const RESET_QUERY_PARAM = "reset-appview"; + +export const CUSTOM_APP_VIEW_CONFIG_ID = "custom"; + +function isKnownId(id) { + if (id === CUSTOM_APP_VIEW_CONFIG_ID) return true; + return DEFAULT_APP_VIEW_CONFIGS.some((config) => config.id === id); +} + +export function isValidAppViewConfig(config) { + return ( + config && + typeof config === "object" && + typeof config.id === "string" && + isKnownId(config.id) && + typeof config.appViewServiceDid === "string" && + config.appViewServiceDid.length > 0 && + typeof config.chatServiceDid === "string" && + config.chatServiceDid.length > 0 + ); +} + +function defaultConfig() { + const { id, appViewServiceDid, chatServiceDid } = AppViewConfig.BLUESKY; + return { id, appViewServiceDid, chatServiceDid }; +} + +export function getAppViewConfig() { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + try { + const parsed = JSON.parse(raw); + if (isValidAppViewConfig(parsed)) { + return { + id: parsed.id, + appViewServiceDid: parsed.appViewServiceDid, + chatServiceDid: parsed.chatServiceDid, + }; + } + } catch { + // fall through to default + } + } + return defaultConfig(); +} + +export function setAppViewConfig(config) { + if (!isValidAppViewConfig(config)) { + throw new Error("Invalid app view config"); + } + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + id: config.id, + appViewServiceDid: config.appViewServiceDid, + chatServiceDid: config.chatServiceDid, + }), + ); +} + +export function resetAppViewConfig() { + localStorage.removeItem(STORAGE_KEY); +} + +export function handleAppViewResetQueryParam() { + const params = new URLSearchParams(window.location.search); + if (!params.has(RESET_QUERY_PARAM)) { + return false; + } + resetAppViewConfig(); + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete(RESET_QUERY_PARAM); + window.history.replaceState({}, "", newUrl.toString()); + return true; +} diff --git a/src/js/config.js b/src/js/config.js index 2456be93..53a47f99 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -30,3 +30,23 @@ export const IN_APP_LINK_DOMAINS = [ "dev.impro.social", "localhost", ]; + +export const AppViewConfig = { + BLUESKY: { + id: "bluesky", + displayName: "Bluesky", + appViewServiceDid: "did:web:api.bsky.app#bsky_appview", + chatServiceDid: "did:web:api.bsky.chat#bsky_chat", + }, + BLACKSKY: { + id: "blacksky", + displayName: "Blacksky", + appViewServiceDid: "did:web:api.blacksky.community#bsky_appview", + chatServiceDid: "did:web:api.blacksky.community#bsky_chat", + }, +}; + +export const DEFAULT_APP_VIEW_CONFIGS = [ + AppViewConfig.BLUESKY, + AppViewConfig.BLACKSKY, +]; diff --git a/src/js/views/login.view.js b/src/js/views/login.view.js index 12904a9d..69f632de 100644 --- a/src/js/views/login.view.js +++ b/src/js/views/login.view.js @@ -7,26 +7,67 @@ import { AuthError, } from "/js/auth.js"; import { html, render } from "/js/lib/lit-html.js"; +import { AppViewConfig, DEFAULT_APP_VIEW_CONFIGS } from "/js/config.js"; +import { + getAppViewConfig, + setAppViewConfig, + isValidAppViewConfig, + CUSTOM_APP_VIEW_CONFIG_ID, +} from "/js/appViewConfig.js"; class LoginView extends View { async render({ root, params, context }) { await requireNoAuth(); + const storedConfig = getAppViewConfig(); + const isStoredCustom = storedConfig.id === CUSTOM_APP_VIEW_CONFIG_ID; + const advancedOpenByDefault = storedConfig.id !== AppViewConfig.BLUESKY.id; + const state = { loading: false, errorMessage: null, + appViewSelection: storedConfig.id, + customAppViewServiceDid: isStoredCustom + ? storedConfig.appViewServiceDid + : "", + customChatServiceDid: isStoredCustom ? storedConfig.chatServiceDid : "", }; const auth = await getAuth(); const isBasicAuth = auth instanceof BasicAuth; + function resolveSelectedAppViewConfig() { + if (state.appViewSelection === CUSTOM_APP_VIEW_CONFIG_ID) { + return { + id: CUSTOM_APP_VIEW_CONFIG_ID, + appViewServiceDid: state.customAppViewServiceDid.trim(), + chatServiceDid: state.customChatServiceDid.trim(), + }; + } + return ( + DEFAULT_APP_VIEW_CONFIGS.find( + (config) => config.id === state.appViewSelection, + ) ?? AppViewConfig.BLUESKY + ); + } + async function handleSubmit(e) { e.preventDefault(); const handle = e.target.handle.value; const password = isBasicAuth ? e.target.password.value : null; + + const selectedConfig = resolveSelectedAppViewConfig(); + if (!isValidAppViewConfig(selectedConfig)) { + state.errorMessage = "Invalid AppView configuration"; + renderPage(); + return; + } + state.loading = true; renderPage(); try { + setAppViewConfig(selectedConfig); + // allow truncated handles let fullHandle = handle.includes(".") ? handle @@ -50,7 +91,21 @@ class LoginView extends View { } } + function handleAppViewChange(e) { + state.appViewSelection = e.target.value; + renderPage(); + } + + function handleCustomAppViewDidInput(e) { + state.customAppViewServiceDid = e.target.value; + } + + function handleCustomChatDidInput(e) { + state.customChatServiceDid = e.target.value; + } + function renderPage() { + const isCustom = state.appViewSelection === CUSTOM_APP_VIEW_CONFIG_ID; render( html`