Compare commits

..

No commits in common. "ae9e7e17ed0a0e3fea951cb1d789f742cce94ea6" and "7d1d8cf8c06115130bb0d8eb27be602b600fad91" have entirely different histories.

10 changed files with 44 additions and 183 deletions

View file

@ -1,6 +1,5 @@
import type { RunnerController } from './components/runner/Runner.tsx'
import { ColorModeProvider, ColorModeScript } from '@kobalte/core'
import { workerInit } from 'mtcute-repl-worker/client'
import { createSignal, lazy, onCleanup, onMount, Show } from 'solid-js'
import { EditorTabs } from './components/editor/EditorTabs.tsx'
@ -17,7 +16,6 @@ export function App() {
const [updating, setUpdating] = createSignal(true)
const [showSettings, setShowSettings] = createSignal(false)
const [settingsTab, setSettingsTab] = createSignal<SettingsTab>('accounts')
const [runnerController, setRunnerController] = createSignal<RunnerController>()
const [isResizing, setIsResizing] = createSignal(false)
const [sizes, setSizes] = createSignal([0.5, 0.5])
@ -78,10 +76,7 @@ export function App() {
<Resizable sizes={sizes()} onSizesChange={e => setSizes(e)} orientation="horizontal" class="size-full max-h-[calc(100vh-57px)]">
<ResizablePanel class="h-full overflow-x-auto overflow-y-hidden" minSize={0.2}>
<EditorTabs />
<Editor
class="size-full"
onRun={() => runnerController()?.run()}
/>
<Editor class="size-full" />
</ResizablePanel>
<ResizableHandle
withHandle
@ -95,10 +90,7 @@ export function App() {
class="flex max-h-full flex-col overflow-hidden"
minSize={0.2}
>
<Runner
isResizing={isResizing()}
controllerRef={setRunnerController}
/>
<Runner isResizing={isResizing()} />
</ResizablePanel>
</Resizable>
</Show>

View file

@ -1,5 +1,5 @@
import { useColorModeValue } from '@kobalte/core'
import { KeyCode, KeyMod, editor as mEditor, Uri } from 'monaco-editor'
import { editor as mEditor, Uri } from 'monaco-editor'
import { createEffect, on, onMount } from 'solid-js'
import { $activeTab, $tabs, type EditorTab } from '../../store/tabs.ts'
@ -9,7 +9,6 @@ import './Editor.css'
export interface EditorProps {
class?: string
onRun: () => void
}
const DEFAULT_CODE = `
@ -23,8 +22,6 @@ export const self = await tg.getMe()
console.log(self)
`.trimStart()
const LOCAL_STORAGE_PREFIX = 'repl:tab-content:'
function findChangedTab(a: EditorTab[], b: EditorTab[]) {
const set = new Set(a.map(tab => tab.id))
for (const tab of b) {
@ -42,6 +39,7 @@ export default function Editor(props: EditorProps) {
let editor: mEditor.IStandaloneCodeEditor | undefined
const monacoTheme = useColorModeValue('latte', 'mocha')
// const monacoTheme = () => scheme() === 'dark' ? 'ayu-dark' : 'ayu-light'
const modelsByTab = new Map<string, mEditor.ITextModel>()
onMount(async () => {
@ -77,24 +75,15 @@ export default function Editor(props: EditorProps) {
await setupMonaco()
for (const tab of tabs()) {
const storedCode = localStorage.getItem(LOCAL_STORAGE_PREFIX + tab.id)
const model = mEditor.createModel(storedCode ?? (tab.main ? DEFAULT_CODE : ''), 'typescript', Uri.parse(`file:///${tab.id}.ts`))
const model = mEditor.createModel(tab.main ? DEFAULT_CODE : '', 'typescript', Uri.parse(`file:///${tab.id}.ts`))
modelsByTab.set(tab.id, model)
}
editor.setModel(modelsByTab.get(activeTab())!)
editor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, () => {
props.onRun()
})
editor.onDidChangeModelContent(() => {
const currentTab = tabs().find(tab => tab.id === activeTab())!
const content = editor?.getModel()?.getValue()
if (!currentTab || !content) return
localStorage.setItem(LOCAL_STORAGE_PREFIX + currentTab.id, content)
})
// editor.onDidChangeModelContent(() => {
// props.onCodeChange(editor?.getValue() ?? '')
// })
return () => editor?.dispose()
})
@ -147,7 +136,6 @@ export default function Editor(props: EditorProps) {
if (!changed) return
modelsByTab.get(changed.id)?.dispose()
modelsByTab.delete(changed.id)
localStorage.removeItem(LOCAL_STORAGE_PREFIX + changed.id)
}
}))

View file

@ -1,14 +1,12 @@
import type { DropdownMenuTriggerProps } from '@kobalte/core/dropdown-menu'
import type { mtcute } from 'mtcute-repl-worker/client'
import type { Setter } from 'solid-js'
import type { CustomTypeScriptWorker } from '../editor/utils/custom-worker.ts'
import { timers } from '@fuman/utils'
import { persistentAtom } from '@nanostores/persistent'
import { LucideCheck, LucidePlay, LucidePlug, LucideRefreshCw, LucideSettings2, LucideSkull, LucideUnplug } from 'lucide-solid'
import { LucideCheck, LucidePlay, LucidePlug, LucideRefreshCw, LucideSkull, LucideUnplug } from 'lucide-solid'
import { languages, Uri } from 'monaco-editor/esm/vs/editor/editor.api.js'
import { createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
import { Dynamic } from 'solid-js/web'
import { toast } from 'solid-sonner'
import { Button } from '../../lib/components/ui/button.tsx'
import {
DropdownMenu,
@ -39,14 +37,7 @@ const $enableVerbose = persistentAtom('repl:verboseLogs', false, {
decode: value => value === 'true',
})
export interface RunnerController {
run: () => void
}
export function Runner(props: {
isResizing: boolean
controllerRef: Setter<RunnerController | undefined>
}) {
export function Runner(props: { isResizing: boolean }) {
const [devtoolsIframe, setDevtoolsIframe] = createSignal<HTMLIFrameElement | undefined>()
const [runnerIframe, setRunnerIframe] = createSignal<HTMLIFrameElement>()
const [runnerLoaded, setRunnerLoaded] = createSignal(false)
@ -163,10 +154,6 @@ export function Runner(props: {
onMount(async () => {
window.addEventListener('message', handleMessage)
props.controllerRef({
run: () => handleRun(),
})
})
onCleanup(() => {
window.removeEventListener('message', handleMessage)
@ -181,11 +168,6 @@ export function Runner(props: {
}, { defer: true }))
async function handleRun() {
if ($activeAccountId.get() === undefined) {
toast('You need to log in to run a script')
return
}
const getWorker = await languages.typescript.getTypeScriptWorker()
const worker = await getWorker(Uri.parse('file:///main.ts')) as unknown as CustomTypeScriptWorker
@ -268,7 +250,14 @@ export function Runner(props: {
</Button>
)}
<div class="flex-1" />
<div class="mr-2 flex items-center text-xs font-medium">
<DropdownMenu>
<DropdownMenuTrigger
as={(props: DropdownMenuTriggerProps) => (
<Button
variant="ghost"
size="xs"
{...props}
>
{{
offline: 'Disconnected',
connecting: 'Connecting...',
@ -285,17 +274,6 @@ export function Runner(props: {
}[connectionState()],
)}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger
as={(props: DropdownMenuTriggerProps) => (
<Button
variant="ghost"
size="icon"
class="size-7"
{...props}
>
<LucideSettings2 class="size-4" />
</Button>
)}
/>

View file

@ -13,7 +13,6 @@ import {
LucideRefreshCw,
LucideSearch,
LucideTrash,
LucideTriangleAlert,
LucideUser,
LucideX,
} from 'lucide-solid'
@ -23,7 +22,6 @@ import { nanoid } from 'nanoid'
import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from 'solid-js'
import { toast } from 'solid-sonner'
import { copyToClipboard } from '../../lib/clipboard.tsx'
import { Alert, AlertDescription, AlertTitle } from '../../lib/components/ui/alert.tsx'
import { Badge } from '../../lib/components/ui/badge.tsx'
import { Button } from '../../lib/components/ui/button.tsx'
import { Dialog, DialogContent } from '../../lib/components/ui/dialog.tsx'
@ -300,7 +298,7 @@ export function AccountsTab() {
return accounts()
}
return accounts()?.filter((account) => {
return accounts().filter((account) => {
return account.name.toLowerCase().includes(query) || account.telegramId.toString().includes(query)
})
})
@ -308,25 +306,9 @@ export function AccountsTab() {
return (
<>
<Show
when={accounts()?.length !== 0}
when={accounts().length !== 0}
fallback={(
<div class="flex h-full flex-col items-center justify-center gap-4 px-2 text-muted-foreground">
<Alert variant="destructive" class="max-w-md">
<LucideTriangleAlert class="size-4" />
<AlertTitle class="font-bold">Warning</AlertTitle>
<AlertDescription>
This is an
{' '}
<b>unofficial</b>
{' '}
Telegram application.
<br />
You might trigger anti-spam measures and get banned.
<br />
Proceed at your own risk.
</AlertDescription>
</Alert>
<div class="flex h-full flex-col items-center justify-center gap-4 text-muted-foreground">
No accounts yet
<div class="flex flex-row gap-2">
<Button

View file

@ -25,7 +25,7 @@ export function TdataImportDialog(props: {
const [error, setError] = createSignal<string | undefined>('')
const [loading, setLoading] = createSignal(false)
const accountExists = (id: number) => $accounts.get()?.some(it => it.telegramId === id)
const accountExists = (id: number) => $accounts.get().some(it => it.telegramId === id)
let abortController: AbortController | undefined
const handleSubmit = async () => {

View file

@ -81,7 +81,7 @@ function QrLoginStep(props: StepProps<'qr'>) {
/>
) : <Spinner indeterminate class="size-10" />}
</div>
<ol class="mt-4 list-inside list-decimal text-sm text-muted-foreground">
<ol class="text-muted-foreground mt-4 list-inside list-decimal text-sm">
<li>Open Telegram on your phone</li>
<li>
Go to
@ -147,7 +147,7 @@ function PhoneNumberStep(props: StepProps<'phone'>) {
<h2 class="mt-4 text-xl font-bold">
Log in with phone number
</h2>
<div class="mt-2 text-center text-sm text-muted-foreground">
<div class="text-muted-foreground mt-2 text-center text-sm">
Please confirm your country code
<br />
and enter your phone number
@ -177,7 +177,7 @@ function PhoneNumberStep(props: StepProps<'phone'>) {
<TextFieldErrorMessage>{error()}</TextFieldErrorMessage>
</TextFieldRoot>
<div class="flex-1" />
<div class="text-center text-sm text-muted-foreground">
<div class="text-muted-foreground text-center text-sm">
or,
{' '}
<a
@ -309,7 +309,7 @@ function OtpStep(props: StepProps<'otp'>) {
Wrong number?
</div>
<div class="mt-4 text-center text-sm text-muted-foreground">
<div class="text-muted-foreground mt-4 text-center text-sm">
{description()}
</div>
<div class="mt-4 flex flex-col items-center text-center">
@ -363,7 +363,7 @@ function OtpStep(props: StepProps<'otp'>) {
</OTPFieldGroup>
</OTPField>
{error() && (
<div class="mt-1 text-sm text-error-foreground">{error()}</div>
<div class="text-error-foreground mt-1 text-sm">{error()}</div>
)}
</Show>
@ -438,7 +438,7 @@ function PasswordStep(props: StepProps<'password'>) {
<h2 class="text-xl font-bold">
2FA password
</h2>
<div class="mt-4 text-center text-sm text-muted-foreground">
<div class="text-muted-foreground mt-4 text-center text-sm">
Your account is protected with an additional password.
</div>
<div class="mt-4">
@ -483,9 +483,9 @@ function DoneStep(props: StepProps<'done'>) {
<div class="flex flex-col items-center justify-center">
<AccountAvatar
account={props.ctx.account}
class="mb-4 size-24 animate-scale-up shadow-sm fill-mode-forwards"
class="animate-scale-up fill-mode-forwards mb-4 size-24 shadow-sm"
/>
<div class="animate-fade-out-down text-center font-medium fill-mode-forwards">
<div class="animate-fade-out-down fill-mode-forwards text-center font-medium">
Welcome,
{' '}
{props.ctx.account.name}

View file

@ -1,64 +0,0 @@
import type { AlertRootProps } from '@kobalte/core/alert'
import type { PolymorphicProps } from '@kobalte/core/polymorphic'
import type { VariantProps } from 'class-variance-authority'
import type { ComponentProps, ValidComponent } from 'solid-js'
import { Alert as AlertPrimitive } from '@kobalte/core/alert'
import { cva } from 'class-variance-authority'
import { splitProps } from 'solid-js'
import { cn } from '../../utils.ts'
export const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
},
)
type alertProps<T extends ValidComponent = 'div'> = AlertRootProps<T> &
VariantProps<typeof alertVariants> & {
class?: string
}
export function Alert<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, alertProps<T>>) {
const [local, rest] = splitProps(props as alertProps, ['class', 'variant'])
return (
<AlertPrimitive
class={cn(
alertVariants({
variant: props.variant,
}),
local.class,
)}
{...rest}
/>
)
}
export function AlertTitle(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class'])
return (
<div
class={cn('font-medium leading-5 tracking-tight', local.class)}
{...rest}
/>
)
}
export function AlertDescription(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class'])
return (
<div class={cn('text-sm [&_p]:leading-relaxed', local.class)} {...rest} />
)
}

View file

@ -2,13 +2,13 @@ import type { TelegramAccount } from 'mtcute-repl-worker/client'
import { computed } from 'nanostores'
import { linkedAtom } from './link.ts'
export const $accounts = linkedAtom<TelegramAccount[] | undefined>('accounts')
export const $accounts = linkedAtom<TelegramAccount[]>('accounts')
export const $activeAccountId = linkedAtom<string | undefined>('activeAccountId')
export const $activeAccount = computed([$accounts, $activeAccountId], (accounts, activeAccountId) => {
if (!activeAccountId) return null
const account = accounts?.find(account => account.id === activeAccountId)
const account = accounts.find(account => account.id === activeAccountId)
if (!account) return null
return account

View file

@ -1,4 +1,3 @@
import { persistentAtom } from '@nanostores/persistent'
import { atom } from 'nanostores'
export interface EditorTab {
@ -7,15 +6,12 @@ export interface EditorTab {
main: boolean
}
export const $tabs = persistentAtom<EditorTab[]>('repl:tabs', [
export const $tabs = atom<EditorTab[]>([
{
id: 'main',
fileName: 'main.ts',
main: true,
},
], {
encode: JSON.stringify,
decode: JSON.parse,
})
])
export const $activeTab = atom('main')

View file

@ -141,16 +141,6 @@ window.addEventListener('message', async ({ data }) => {
currentScriptId = nanoid()
await swInvokeMethodInner({ event: 'UPLOAD_SCRIPT', name: currentScriptId, files: data.files }, asNonNull(navigator.serviceWorker.controller))
if (!window.tg) {
// shouldnt happen but just in case
console.warn('[mtcute-repl] Telegram client not initialized yet')
return
}
if (lastConnectionState === 'offline') {
await window.tg.connect()
}
const el = document.createElement('script')
el.type = 'module'
let script = `import * as result from "/sw/runtime/script/${currentScriptId}/main.js";`
@ -187,7 +177,6 @@ window.addEventListener('message', async ({ data }) => {
initClient(lastAccountId, data.verboseLogs)
}
window.parent.postMessage({ event: 'CONNECTION_STATE', value: 'offline' }, HOST_ORIGIN)
lastConnectionState = 'offline'
} else if (data.event === 'RECONNECT') {
if (window.tg !== undefined) {
window.tg.connect()