feat: tl reference web application

available at https://mt.tei.su/tl/
This commit is contained in:
teidesu 2021-04-14 18:08:53 +03:00
parent eae2c7f459
commit 958dd60c75
37 changed files with 12360 additions and 153 deletions

View file

@ -9,3 +9,7 @@ insert_final_newline = true
max_line_length = 120
tab_width = 4
trim_trailing_whitespace = true
[*.yaml]
indent_size = 2
tab_width = 2

5
.gitignore vendored
View file

@ -4,3 +4,8 @@ private/
.nyc_output/
*.log
# Docs are generated and deployed in gh-pages branch.
docs/*
!docs/.nojekyll

0
docs/.nojekyll Normal file
View file

3
packages/tl-reference/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
.cache/
public

View file

@ -0,0 +1,6 @@
# @mtcute/tl-reference
[https://mt.tei.su/tl](https://mt.tei.su/tl)
Small Gatsby application that allows easy browsing
through Telegram APIs.

View file

@ -0,0 +1,6 @@
This directory contains historical data about the TL schema.
That is, it contains history of the schema over time and pre-calculated difference
between consecutive versions.
The files are generated with `fetch-history.js` script and are not in the repository.

View file

@ -0,0 +1 @@
*.json

View file

@ -0,0 +1 @@
*.json

View file

@ -0,0 +1,34 @@
module.exports = {
siteMetadata: {
title: 'TL reference',
},
pathPrefix: '/tl',
plugins: [
'gatsby-theme-material-ui',
'gatsby-transformer-json',
'gatsby-plugin-sass',
'gatsby-plugin-react-helmet',
{
resolve: 'gatsby-plugin-nprogress',
options: {
color: 'white'
},
},
{
resolve: 'gatsby-source-filesystem',
options: {
path: './data',
ignore: [
'./data/history/last-fetched.json',
'./data/README.md',
],
},
},
{
resolve: 'gatsby-plugin-layout',
options: {
component: require.resolve('./src/layout.tsx'),
},
},
],
}

View file

@ -0,0 +1,137 @@
const rawSchema = require('@mtcute/tl/raw-schema')
const rawErrors = require('@mtcute/tl/raw-errors')
const path = require('path')
const { convertToArrays, prepareData } = require('./scripts/prepare-data')
const TL_NODE_TYPE = 'TlObject'
exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
const { createNode } = actions
function createForNs(ns, prefix = '') {
ns.classes.forEach((cls) => {
createNode({
...cls,
id: createNodeId(`${TL_NODE_TYPE}-class-${prefix}${cls.name}`),
parent: null,
children: [],
type: 'class',
prefix,
internal: {
type: TL_NODE_TYPE,
content: JSON.stringify(cls),
contentDigest: createContentDigest(cls),
},
})
})
ns.methods.forEach((cls) => {
createNode({
...cls,
id: createNodeId(`${TL_NODE_TYPE}-method-${prefix}${cls.name}`),
parent: null,
children: [],
type: 'method',
prefix,
internal: {
type: TL_NODE_TYPE,
content: JSON.stringify(cls),
contentDigest: createContentDigest(cls),
},
})
})
ns.unions.forEach((cls) => {
createNode({
...cls,
name: cls.type,
id: createNodeId(`${TL_NODE_TYPE}-union-${prefix}${cls.type}`),
parent: null,
children: [],
type: 'union',
prefix,
internal: {
type: TL_NODE_TYPE,
content: JSON.stringify(cls),
contentDigest: createContentDigest(cls),
},
})
})
}
const mtproto = convertToArrays(rawSchema.mtproto)
const api = convertToArrays(rawSchema.api)
prepareData(mtproto)
prepareData(api)
createForNs(mtproto, 'mtproto/')
createForNs(api)
}
const TLObject = path.resolve('./src/templates/tl-object.tsx')
const TlTypesList = path.resolve('./src/templates/tl-types-list.tsx')
exports.createPages = async ({ graphql, actions }) => {
const result = await graphql(`
query {
allTlObject {
nodes {
prefix
name
type
namespace
subtypes
}
}
}
`)
result.data.allTlObject.nodes.forEach((node) => {
actions.createPage({
path: `${node.prefix}${node.type}/${node.name}`,
component: TLObject,
context: {
...node,
hasSubtypes: !!node.subtypes,
},
})
})
const result2 = await graphql(`
query {
allTlObject {
group(field: prefix) {
fieldValue
nodes {
namespace
}
}
}
}
`)
result2.data.allTlObject.group.forEach(({ fieldValue: prefix, nodes }) => {
const namespaces = [...new Set(nodes.map(i => i.namespace))]
namespaces.forEach((ns) => {
let namespace
if (ns === '$root') namespace = ''
else namespace = '/' + ns
;(['types', 'methods']).forEach((type) => {
actions.createPage({
path: `${prefix}${type}${namespace}`,
component: TlTypesList,
context: {
prefix,
ns,
type,
isTypes: type === 'types',
isMethods: type === 'methods'
}
})
})
})
})
}

View file

@ -0,0 +1,43 @@
{
"name": "tl-reference",
"version": "1.0.0",
"private": true,
"description": "TL reference",
"author": "Alisa Sireneva <me@tei.su>",
"scripts": {
"develop": "gatsby develop --port 4040",
"start": "gatsby develop",
"build": "gatsby build --prefix-paths",
"postbuild": "(rm -rf ../../docs/tl || rd /s /q ..\\..\\docs\\tl) && (mv public ../../docs/tl || move public ../../docs/tl)",
"serve": "gatsby serve --prefix-paths",
"clean": "gatsby clean"
},
"dependencies": {
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"@types/node": "^14.14.37",
"@types/react-helmet": "^6.1.1",
"gatsby": "^3.2.1",
"gatsby-cli": "^3.2.0",
"gatsby-source-filesystem": "^3.2.0",
"gatsby-transformer-json": "^3.2.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"@mtcute/tl": "^0.0.0",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"gatsby-theme-material-ui": "^2.0.1",
"gatsby-plugin-layout": "^2.2.0",
"gatsby-plugin-nprogress": "^3.3.0",
"sass": "^1.32.8",
"gatsby-plugin-sass": "^4.2.0",
"fuse.js": "^6.4.6",
"gatsby-plugin-react-helmet": "^4.3.0",
"react-helmet": "^6.1.0",
"lodash.throttle": "^4.1.1"
},
"devDependencies": {
"node-fetch": "^2.6.1",
"marked": "^2.0.3"
}
}

View file

@ -0,0 +1,388 @@
const fs = require('fs')
const path = require('path')
const { convertTlToJson } = require('../../tl/scripts/generate-schema')
const fetch = require('node-fetch')
const qs = require('querystring')
const { convertToArrays } = require('./prepare-data')
const UNIX_0 = '1970-01-01T00:00:00Z'
const CURRENT_FILE = 'Telegram/Resources/tl/api.tl'
const FILES = [
'Telegram/SourceFiles/mtproto/scheme.tl',
'Telegram/Resources/scheme.tl',
CURRENT_FILE,
]
async function getLastFetched() {
return fs.promises
.readFile(path.join(__dirname, '../data/history/last-fetched.json'), 'utf8')
.then((res) => JSON.parse(res))
.catch(() => ({
...FILES.reduce((a, b) => {
a[b] = UNIX_0
return a
}, {}),
}))
}
async function updateLastFetched(file, time) {
return getLastFetched().then((state) =>
fs.promises.writeFile(
path.join(__dirname, '../data/history/last-fetched.json'),
JSON.stringify({
...state,
[file]: time,
})
)
)
}
async function getFileContent(file, commit) {
return fetch(
`https://raw.githubusercontent.com/telegramdesktop/tdesktop/${commit}/${file}`
).then((r) => r.text())
}
async function parseRemoteTl(file, commit) {
let content = await getFileContent(file, commit)
if (content === '404: Not Found') return null
let layer = (function () {
const m = content.match(/^\/\/ LAYER (\d+)/m)
if (m) return m[1]
return null
})()
if (!layer) {
// older files did not contain layer number in comment.
if (content.match(/invokeWithLayer#da9b0d0d/)) {
// if this is present, then the layer number is available in
// Telegram/SourceFiles/mtproto/mtpCoreTypes.h
let mtpCoreTypes = await getFileContent(
'Telegram/SourceFiles/mtproto/mtpCoreTypes.h',
commit
)
if (mtpCoreTypes === '404: Not Found') {
mtpCoreTypes = await getFileContent(
'Telegram/SourceFiles/mtproto/core_types.h',
commit
)
}
const m = mtpCoreTypes.match(
/^static const mtpPrime mtpCurrentLayer = (\d+);$/m
)
if (!m)
throw new Error(
`Could not determine layer number for file ${file} at commit ${commit}`
)
layer = m[1]
} else {
// even older files on ancient layers
// layer number is the largest available invokeWithLayerN constructor
let max = 0
content.replace(/invokeWithLayer(\d+)#[0-f]+/g, (_, $1) => {
$1 = parseInt($1)
if ($1 > max) max = $1
})
if (max === 0)
throw new Error(
`Could not determine layer number for file ${file} at commit ${commit}`
)
layer = max + ''
}
}
if (content.match(/bad_server_salt#/)) {
// this is an older file that contained both mtproto and api
// since we are only interested in api, remove the mtproto part
const lines = content.split('\n')
const apiIdx = lines.indexOf('///////// Main application API')
if (apiIdx === -1)
throw new Error('Could not find split point for combined file')
content = lines.slice(apiIdx).join('\n')
}
return {
layer,
content,
tl: await convertTlToJson(content, 'api', true),
}
}
function createTlDifference(old, mod) {
const diff = {
added: { classes: [], methods: [], unions: [] },
removed: { classes: [], methods: [], unions: [] },
modified: { classes: [], methods: [], unions: [] },
}
old = convertToArrays(old.tl)
mod = convertToArrays(mod.tl)
// create index for both old and mod
const { oldIndex, modIndex } = (function () {
function createIndex(it) {
let ret = {}
it.classes.forEach((obj) => {
obj.uid = 'c_' + obj.name
obj._type = 'classes'
ret[obj.uid] = obj
})
it.methods.forEach((obj) => {
obj.uid = 'm_' + obj.name
obj._type = 'methods'
ret[obj.uid] = obj
})
it.unions.forEach((obj) => {
obj.uid = 'u_' + obj.type
obj._type = 'unions'
ret[obj.uid] = obj
})
return ret
}
return {
oldIndex: createIndex(old),
modIndex: createIndex(mod),
}
})()
// find difference between constructor arguments
function createArgsDifference(old, mod) {
const diff = {
added: [],
removed: [],
modified: [],
}
const { oldIndex, modIndex } = (function () {
function createIndex(obj) {
const ret = {}
if (obj.arguments)
obj.arguments.forEach((arg) => (ret[arg.name] = arg))
return ret
}
return {
oldIndex: createIndex(old),
modIndex: createIndex(mod),
}
})()
Object.keys(modIndex).forEach((argName) => {
if (!(argName in oldIndex)) {
diff.added.push(modIndex[argName])
} else {
const old = oldIndex[argName]
const mod = modIndex[argName]
if (
old.type !== mod.type ||
old.optional !== mod.optional ||
mod.predicate !== mod.predicate
) {
diff.modified.push({
name: argName,
old: old,
new: mod,
})
}
}
})
Object.keys(oldIndex).forEach((argName) => {
if (!(argName in modIndex)) {
diff.removed.push(oldIndex[argName])
}
})
return diff
}
Object.keys(modIndex).forEach((uid) => {
if (!(uid in oldIndex)) {
diff.added[modIndex[uid]._type].push(modIndex[uid])
} else {
const old = oldIndex[uid]
const mod = modIndex[uid]
const localDiff = {}
const argDiff = createArgsDifference(old, mod)
if (
argDiff.removed.length ||
argDiff.added.length ||
argDiff.modified.length
) {
localDiff.arguments = argDiff
}
if (old.id !== mod.id) localDiff.id = { old: old.id, new: mod.id }
if (old.type !== mod.type)
localDiff.type = { old: old.type, new: mod.type }
if (old.returns !== mod.returns)
localDiff.returns = { old: old.returns, new: mod.returns }
if (Object.keys(localDiff).length) {
localDiff.name = old.name
diff.modified[oldIndex[uid]._type].push(localDiff)
}
}
})
Object.keys(oldIndex).forEach((uid) => {
if (!(uid in modIndex)) {
diff.removed[oldIndex[uid]._type].push(oldIndex[uid])
}
})
return diff
}
function fileSafeDateFormat(date) {
date = new Date(date)
return date.toISOString().replace(/[\-:]|\.\d\d\d/g, '')
}
function shortSha(sha) {
return sha.substr(0, 7)
}
async function fetchHistory(file, since, defaultParent = null) {
const history = await (async function () {
const ret = []
let page = 1
while (true) {
const chunk = await fetch(
`https://api.github.com/repos/telegramdesktop/tdesktop/commits?` +
qs.stringify({
since,
path: file,
per_page: 100,
page,
})
).then((r) => r.json())
if (!chunk.length) break
ret.push(...chunk)
page += 1
}
return ret
})()
// should not happen
if (history.length === 0) throw new Error('history is empty')
const filename = (schema, commit) =>
`layer${schema.layer}-${fileSafeDateFormat(
commit.commit.committer.date
)}-${shortSha(commit.sha)}.json`
function writeSchemaToFile(schema, commit) {
return fs.promises.writeFile(
path.join(__dirname, `../data/history/${filename(schema, commit)}`),
JSON.stringify({
tl: JSON.stringify(schema.tl),
layer: parseInt(schema.layer),
content: schema.content,
// idk where parent: '00' comes from but whatever
parent: schema.parent && schema.parent !== '00' ? schema.parent : defaultParent,
source: {
file,
date: commit.commit.committer.date,
commit: commit.sha,
message: commit.message,
},
})
)
}
let base = history.pop()
let baseSchema = await parseRemoteTl(file, base.sha)
let baseFilename = () => filename(baseSchema, base)
try {
await fs.promises.access(
path.join(__dirname, `../data/history/${baseFilename()}`),
fs.F_OK
)
} catch (e) {
await writeSchemaToFile(baseSchema, base)
}
while (history.length) {
const next = history.pop()
const nextSchema = await parseRemoteTl(file, next.sha)
if (!nextSchema) break
const diff = createTlDifference(baseSchema, nextSchema)
await fs.promises.writeFile(
path.join(
__dirname,
`../data/diffs/${shortSha(base.sha)}-${shortSha(next.sha)}.json`
),
JSON.stringify({
...diff,
// yeah they sometimes update schema w/out changing layer number
layer:
baseSchema.layer === nextSchema.layer
? undefined
: nextSchema.layer,
})
)
nextSchema.parent = baseFilename()
base = next
baseSchema = nextSchema
await updateLastFetched(file, base.commit.committer.date)
await writeSchemaToFile(baseSchema, base)
console.log(
'Fetched commit %s, file %s (%s)',
shortSha(base.sha),
file,
base.commit.committer.date
)
}
if (file !== CURRENT_FILE) {
await updateLastFetched(file, 'DONE:' + baseFilename())
}
console.log('No more commits for %s', file)
}
async function main() {
const last = await getLastFetched()
for (let i = 0; i < FILES.length; i++) {
const file = FILES[i]
const prev = FILES[i - 1]
if (!last[file].startsWith('DONE')) {
let parent = prev ? last[prev].split(':')[1] : null
await fetchHistory(file, last[file], parent)
}
}
console.log('Creating reverse links ("next" field)')
for (const file of await fs.promises.readdir(
path.join(__dirname, '../data/history')
)) {
if (!file.startsWith('layer')) continue
const fullPath = path.join(__dirname, '../data/history', file)
const json = JSON.parse(await fs.promises.readFile(fullPath, 'utf-8'))
if (json.parent) {
const parentPath = path.join(__dirname, '../data/history', json.parent)
const parentJson = JSON.parse(
await fs.promises.readFile(
parentPath,
'utf-8'
)
)
parentJson.next = parentPath
await fs.promises.writeFile(parentPath, JSON.stringify(parentJson))
}
}
}
main().catch(console.error)

View file

@ -0,0 +1,156 @@
// converts object-based schema to array-based
function convertToArrays(ns) {
const ret = {
classes: [],
methods: [],
unions: [],
}
Object.entries(ns).forEach(([ns, content]) => {
const prefix = ns === '$root' ? '' : `${ns}.`
content.classes.forEach((cls) => {
cls.rawName = cls.name
cls.name = prefix + cls.name
cls.namespace = ns
ret.classes.push(cls)
})
content.methods.forEach((cls) => {
cls.rawName = cls.name
cls.name = prefix + cls.name
cls.namespace = ns
ret.methods.push(cls)
})
content.unions.forEach((cls) => {
cls.rawName = cls.type
cls.type = prefix + cls.type
cls.namespace = ns
ret.unions.push(cls)
})
})
return ret
}
const marked = require('marked')
const pascalToCamel = (s) => s[0].toLowerCase() + s.substr(1)
const camelToSnake = (str) =>
str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
const camelToPascal = (s) => s[0].toUpperCase() + s.substr(1)
function renderDescription(description) {
return marked.parseInline(
description.replace(/{@link (.+?)}/g, (_, name) => {
if (name.startsWith('tl.')) {
let [ns, type] = name.substr(3).split('.')
if (!type) {
type = ns
ns = undefined
}
let path, displayName, m
if ((m = type.match(/^Raw([A-Za-z0-9_]+?)(Request)?$/))) {
const [, name, isMethod] = m
path = `${ns === 'mtproto' ? ns : ''}/${
isMethod ? 'method' : 'class'
}/${ns !== 'mtproto' ? ns + '.' : ''}${pascalToCamel(name)}`
displayName =
(ns ? ns + (ns === 'mtproto' ? '/' : '.') : '') +
pascalToCamel(name)
} else if ((m = type.match(/^Type([A-Za-z0-9_]+?)$/))) {
path = `${ns === 'mtproto' ? ns : ''}/union/${
ns !== 'mtproto' ? ns + '.' : ''
}${m[1]}`
displayName =
(ns ? ns + (ns === 'mtproto' ? '/' : '.') : '') +
pascalToCamel(name)
}
if (path) {
return `[${displayName}](/${path})`
}
}
return `\`${name}\``
})
)
}
function prepareData(data) {
Object.values(data).forEach((arr) =>
arr.forEach((item) => {
// add hex constructor id
if (item.id) item.tlId = item.id.toString(16).padStart(8, '0')
// add typescript types for the item and arguments
// basically copy-pasted from generate-types.js
const prefix_ = item.prefix === 'mtproto/' ? 'mt_' : ''
let baseTypePrefix =
item.prefix === 'mtproto/' ? 'tl.mtproto.' : 'tl.'
const makePascalCaseNotNamespace = (type) => {
let split = type.split('.')
let name = split.pop()
let ns = split
if (!ns.length) {
if (name[0].match(/[A-Z]/))
// this is union/alias
return 'Type' + name
return 'Raw' + camelToPascal(name)
}
if (name[0].match(/[A-Z]/)) return ns.join('.') + '.Type' + name
return ns.join('.') + '.Raw' + camelToPascal(name)
}
const fullTypeName = (type) => {
if (type === 'X') return 'any'
if (type[0] === '%') type = type.substr(1)
if (prefix_ === 'mt_' && type === 'Object') return 'tl.TlObject'
if (
type === 'number' ||
type === 'any' ||
type === 'Long' ||
type === 'RawLong' ||
type === 'Int128' ||
type === 'Int256' ||
type === 'Double' ||
type === 'string' ||
type === 'Buffer' ||
type.match(/^(boolean|true|false)$/)
)
return type
if (type.endsWith('[]')) {
let wrap = type.substr(0, type.length - 2)
return fullTypeName(wrap) + '[]'
}
return baseTypePrefix + makePascalCaseNotNamespace(type)
}
if (item.subtypes) {
item.ts = 'Type' + item.rawName
} else {
item.ts =
'Raw' +
camelToPascal(item.rawName) +
(item.returns ? 'Request' : '')
item.underscore = prefix_ + item.name
}
// render descriptions in markdown
if (item.description)
item.description = renderDescription(item.description)
if (item.arguments)
item.arguments.forEach((arg) => {
if (arg.description)
arg.description = renderDescription(arg.description)
arg.ts = fullTypeName(arg.type)
})
})
)
}
module.exports = { convertToArrays, prepareData }

View file

@ -0,0 +1,76 @@
import {
createStyles,
fade,
InputBase,
InputBaseProps,
makeStyles,
Theme,
} from '@material-ui/core'
import React from 'react'
import SearchIcon from '@material-ui/icons/Search'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing(2),
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(3),
width: 'auto',
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: '40ch',
},
},
})
)
export function ActionBarSearchField(
params: Partial<InputBaseProps> & { inputRef?: React.Ref<any> }
): React.ReactElement {
const classes = useStyles()
return (
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
ref={params.inputRef}
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
inputProps={{ 'aria-label': 'search' }}
{...params}
/>
</div>
)
}

View file

@ -0,0 +1,35 @@
import Fuse from 'fuse.js'
import React from 'react'
function highlight(
value: string,
ranges: ReadonlyArray<Fuse.RangeTuple>,
className: string,
pos = ranges.length
): React.ReactElement {
const pair = ranges[pos - 1]
return pair ? (
<>
{highlight(value.substring(0, pair[0]), ranges, className, pos - 1)}
<span className={className}>
{value.substring(pair[0], pair[1] + 1)}
</span>
{value.substring(pair[1] + 1)}
</>
) : (
<span>{value}</span>
)
}
export function FuseHighlight({
matches,
value,
className,
}: {
matches: ReadonlyArray<Fuse.FuseResultMatch>
value: string
className: string
}): React.ReactElement {
return highlight(value, matches![0].indices, className)
}

View file

@ -0,0 +1,242 @@
import { graphql, Link, useStaticQuery } from 'gatsby'
import { ChangeEvent, useState } from 'react'
import { ExtendedTlObject, GraphqlAllResponse } from '../types'
import SearchIcon from '@material-ui/icons/Search'
import {
Avatar,
createStyles,
Fade,
fade,
InputBase,
List,
ListItem,
ListItemAvatar,
ListItemText,
makeStyles,
Paper,
Popper,
Theme,
Button,
ClickAwayListener,
} from '@material-ui/core'
import React from 'react'
import { useLocalState } from '../hooks/use-local-state'
import Fuse from 'fuse.js'
import blue from '@material-ui/core/colors/blue'
import red from '@material-ui/core/colors/red'
import yellow from '@material-ui/core/colors/yellow'
import UnionIcon from '@material-ui/icons/AccountTree'
import ClassIcon from '@material-ui/icons/Class'
import FunctionsIcon from '@material-ui/icons/Functions'
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
import { useFuse } from '../hooks/use-fuse'
import { FuseHighlight } from './fuse-highlight'
import { ActionBarSearchField } from './actionbar-search-field'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
popup: {
display: 'flex',
height: 250,
overflowY: 'auto',
overflowX: 'hidden',
},
popupEmpty: {
padding: theme.spacing(2),
flex: '1 1 auto',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
flexDirection: 'column',
},
popupEmptyIcon: {
fontSize: '48px',
display: 'block',
marginBottom: theme.spacing(2),
},
popupList: {
width: '100%',
},
popupListItem: {
padding: theme.spacing(0, 2),
},
searchItemMatch: {
color: theme.palette.type === 'dark' ? blue[300] : blue[700],
},
})
)
export function GlobalSearchField({ isMobile }: { isMobile: boolean }): React.ReactElement {
const classes = useStyles()
const allObjects: {
allTlObject: GraphqlAllResponse<ExtendedTlObject>
} = useStaticQuery(graphql`
query {
allTlObject {
edges {
node {
id
prefix
type
name
}
}
}
}
`)
const [includeMtproto, setIncludeMtproto] = useLocalState('mtproto', false)
const { hits, query, onSearch } = useFuse(
allObjects.allTlObject.edges,
{
keys: ['node.name'],
includeMatches: true,
threshold: 0.3,
},
{ limit: 25 },
includeMtproto ? undefined : (it) => it.node.prefix !== 'mtproto/'
)
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
const [open, setOpen] = useState(false)
const notFound = () => (
<>
<ErrorOutlineIcon className={classes.popupEmptyIcon} />
Nothing found
{!includeMtproto && (
<Button
variant="text"
size="small"
style={{
margin: '4px auto',
}}
onClick={() => {
setIncludeMtproto(true)
}}
>
Retry including MTProto objects
</Button>
)}
</>
)
const emptyField = () => (
<>
<SearchIcon className={classes.popupEmptyIcon} />
Start typing...
</>
)
const renderSearchItem = (
node: ExtendedTlObject,
matches: ReadonlyArray<Fuse.FuseResultMatch>
) => (
<ListItem
button
divider
component={Link}
to={`/${node.prefix}${node.type}/${node.name}`}
className={classes.popupListItem}
onClick={() => setOpen(false)}
key={node.id}
>
<ListItemAvatar>
<Avatar
style={{
backgroundColor:
node.type === 'class'
? blue[600]
: node.type === 'method'
? red[600]
: yellow[700],
}}
>
{node.type === 'class' ? (
<ClassIcon />
) : node.type === 'method' ? (
<FunctionsIcon />
) : (
<UnionIcon />
)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<>
{node.prefix}
<FuseHighlight
matches={matches}
value={node.name}
className={classes.searchItemMatch}
/>
</>
}
secondary={node.type}
/>
</ListItem>
)
const popupContent = (
<Paper className={classes.popup}>
{query.length <= 1 || !hits.length ? (
<div className={classes.popupEmpty}>
{query.length <= 1 ? emptyField() : notFound()}
</div>
) : (
<List disablePadding dense className={classes.popupList}>
{hits.map(({ item: { node }, matches }) =>
renderSearchItem(node, matches!)
)}
<div style={{ textAlign: 'center' }}>
<Button
variant="text"
size="small"
style={{
margin: '4px auto',
}}
onClick={() => {
setIncludeMtproto(!includeMtproto)
}}
>
{includeMtproto ? 'Hide' : 'Include'} MTProto
objects
</Button>
</div>
</List>
)}
</Paper>
)
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<>
<ActionBarSearchField
inputRef={setAnchorEl}
autoComplete="off"
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
onChange={onSearch}
/>
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom"
transition
style={{
width: isMobile ? '100%' : anchorEl?.clientWidth,
zIndex: 9999,
}}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
{popupContent}
</Fade>
)}
</Popper>
</>
</ClickAwayListener>
)
}

View file

@ -0,0 +1,67 @@
import { Link as MuiLink } from '@material-ui/core'
import { Link } from 'gatsby'
import React from 'react'
import { ExtendedTlObject } from '../../types'
export function LinkToTl(name: string): React.ReactElement
export function LinkToTl(obj: ExtendedTlObject): React.ReactElement
export function LinkToTl(
prefix: string,
type: string,
name: string
): React.ReactElement
export function LinkToTl(
prefix: string | ExtendedTlObject,
type?: string,
name?: string
): React.ReactElement {
if (typeof prefix !== 'string') {
type = prefix.type
name = prefix.name
prefix = prefix.prefix
}
// this kind of invokation is used in parameters table
if (!type && !name) {
const fullType = prefix
// core types
if (
fullType === 'number' ||
fullType === 'Long' ||
fullType === 'Int128' ||
fullType === 'Int256' ||
fullType === 'Double' ||
fullType === 'string' ||
fullType === 'Buffer' ||
fullType === 'boolean' ||
fullType === 'true' ||
fullType === 'any' ||
fullType === '$FlagsBitField'
) {
return (
<MuiLink component={Link} to="/#core-types">
{fullType === '$FlagsBitField' ? 'TlFlags' : fullType}
</MuiLink>
)
}
// array
if (fullType.substr(fullType.length - 2) === '[]') {
return <>{LinkToTl(fullType.substr(0, fullType.length - 2))}[]</>
}
// must be union since this is from parameters type
prefix = ''
type = 'union'
name = fullType
}
return (
<MuiLink component={Link} to={`/${prefix}${type}/${name}`}>
{prefix}
{name}
</MuiLink>
)
}

View file

@ -0,0 +1,183 @@
import { TableOfContents, TableOfContentsItem } from './table-of-contents'
import React, { useState } from 'react'
import {
Box,
Container,
createStyles,
Divider,
Link as MuiLink,
List,
makeStyles,
Paper,
Typography,
} from '@material-ui/core'
import { ExtendedTlObject } from '../types'
import { Link } from 'gatsby'
const useStyles = makeStyles(() =>
createStyles({
container: {
height: '100%',
padding: 16,
display: 'flex',
flexDirection: 'row',
overflowY: 'auto',
},
inner: {
flex: '1 1 auto',
},
box: {
paddingBottom: 32,
},
footer: {
marginTop: 64,
textAlign: 'center',
},
})
)
export const usePageStyles = makeStyles((theme) =>
createStyles({
heading0: {
margin: theme.spacing(4, 0),
},
heading1: {
marginBottom: theme.spacing(4),
},
heading: {
marginTop: theme.spacing(6),
marginBottom: theme.spacing(4),
},
paragraph: {
marginBottom: theme.spacing(2),
},
})
)
export function Page({
toc,
children,
}: {
toc?: TableOfContentsItem[]
children: React.ReactNode
}): React.ReactElement {
const classes = useStyles()
const [container, setContainer] = useState<HTMLElement | null>(null)
return (
<Paper ref={setContainer} elevation={0} className={classes.container}>
<Container maxWidth="md" className={classes.inner}>
<Box className={classes.box}>
{children}
<footer>
<Typography
color="textSecondary"
variant="body2"
className={classes.footer}
>
&copy; MTCute TL reference. This website is{' '}
<MuiLink href="https://github.com/teidesu/mtcute/tree/master/packages/tl-reference">
open-source
</MuiLink>{' '}
and licensed under MIT.<br/>
This website is not affiliated with Telegram.
</Typography>
</footer>
</Box>
</Container>
{toc && <TableOfContents items={toc} container={container} />}
</Paper>
)
}
export function Description(params: {
description: string | null
component?: any
className?: string
}) {
const { description, component: Component, ...other } = params as any
return Component ? (
<Component
{...other}
dangerouslySetInnerHTML={{
__html: description || 'No description available :(',
}}
/>
) : (
<div
{...other}
dangerouslySetInnerHTML={{
__html: description || 'No description available :(',
}}
/>
)
}
export function ListItemTlObject({ node }: { node: ExtendedTlObject }) {
return (
<>
<div style={{ padding: '16px 32px' }}>
<MuiLink
component={Link}
to={`/${node.prefix}${node.type}/${node.name}`}
>
<Typography variant="h5" color="textPrimary">
{node.prefix}
{node.name}
</Typography>
</MuiLink>
<Description description={node.description} />
</div>
<Divider />
</>
)
}
export function Section({
title,
id,
children,
}: {
title?: string
id?: string
children: React.ReactNode
}) {
const pageClasses = usePageStyles()
return (
<>
{title && id && (
<Typography
variant="h4"
id={id}
className={pageClasses.heading}
>
{title}
</Typography>
)}
{children}
</>
)
}
export function SectionWithList({
nodes,
children,
...params
}: Omit<Parameters<typeof Section>[0], 'children'> & {
children?: React.ReactNode
nodes: ExtendedTlObject[]
}) {
return (
<Section {...params}>
{children && <Typography variant="body1">{children}</Typography>}
<List>
{nodes.map((node) => (
<ListItemTlObject node={node} key={node.id} />
))}
</List>
</Section>
)
}

View file

@ -0,0 +1,5 @@
import { styled } from '@material-ui/core'
export const Spacer = styled('div')({
flex: '1 1 auto'
})

View file

@ -0,0 +1,234 @@
import { createStyles, Link, makeStyles, Typography } from '@material-ui/core'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import throttle from 'lodash/throttle'
import React, { MouseEvent } from 'react'
import clsx from 'clsx'
// based on https://github.com/mui-org/material-ui/blob/master/docs/src/modules/components/AppTableOfContents.js
const useStyles = makeStyles((theme) =>
createStyles({
root: {
top: 80,
// Fix IE 11 position sticky issue.
width: 175,
flexShrink: 0,
order: 2,
position: 'sticky',
height: 'calc(100vh - 80px)',
overflowY: 'auto',
padding: theme.spacing(2, 2, 2, 0),
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
contents: {
marginTop: theme.spacing(2),
paddingLeft: theme.spacing(1.5),
},
ul: {
padding: 0,
margin: 0,
listStyleType: 'none',
},
item: {
fontSize: 13,
padding: theme.spacing(0.5, 0, 0.5, 1),
borderLeft: '4px solid transparent',
boxSizing: 'content-box',
'&:hover': {
borderLeft: `4px solid ${
theme.palette.type === 'light'
? theme.palette.grey[200]
: theme.palette.grey[900]
}`,
},
'&$active,&:active': {
borderLeft: `4px solid ${
theme.palette.type === 'light'
? theme.palette.grey[300]
: theme.palette.grey[800]
}`,
},
},
secondaryItem: {
paddingLeft: theme.spacing(2.5),
},
active: {},
})
)
const noop = () => {}
function useThrottledOnScroll(
callback: ((evt: Event) => void) | null,
delay: number,
container: HTMLElement | null
) {
const throttledCallback = useMemo(
() => (callback ? throttle(callback, delay) : noop),
[callback, delay]
)
useEffect(() => {
if (throttledCallback === noop || container === null) {
return undefined
}
container.addEventListener('scroll', throttledCallback)
return () => {
container.removeEventListener('scroll', throttledCallback)
}
}, [throttledCallback, container])
}
export interface TableOfContentsItem {
id: string
title: string
}
interface TocWithNode extends TableOfContentsItem {
node: HTMLElement
}
export function TableOfContents({
items,
container,
}: {
items: TableOfContentsItem[]
container: HTMLElement | null
}): React.ReactElement {
const classes = useStyles()
const itemsWithNodeRef = useRef<TocWithNode[]>([])
useEffect(() => {
itemsWithNodeRef.current = items
? items.map(({ id, title }) => {
return {
id,
title,
node: document.getElementById(id)!,
}
})
: []
}, [items])
const [activeState, setActiveState] = useState<string | null>(items[0]?.id || null)
const clickedRef = useRef(false)
const unsetClickedRef = useRef<NodeJS.Timeout | null>(null)
const findActiveIndex = useCallback(() => {
// Don't set the active index based on scroll if a link was just clicked
if (clickedRef.current || !container) {
return
}
let active
for (let i = itemsWithNodeRef.current.length - 1; i >= 0; i -= 1) {
const item = itemsWithNodeRef.current[i]
if (process.env.NODE_ENV !== 'production') {
if (!item.node) {
console.error(
`Missing node on the item ${JSON.stringify(
item,
null,
2
)}`
)
}
}
if (
item.node &&
item.node.offsetTop <
container.scrollTop + container.clientHeight / 8
) {
active = item
break
}
}
if (active && activeState !== active.id) {
setActiveState(active.id!)
}
}, [activeState, container])
// Corresponds to 10 frames at 60 Hz
useThrottledOnScroll(
items && items.length > 0 ? findActiveIndex : null,
166,
container
)
const handleClick = (id: string) => (
event: MouseEvent<HTMLAnchorElement>
) => {
// Ignore click for new tab/new window behavior
if (
event.defaultPrevented ||
event.button !== 0 || // ignore everything but left-click
event.metaKey ||
event.ctrlKey ||
event.altKey ||
event.shiftKey
) {
return
}
// Used to disable findActiveIndex if the page scrolls due to a click
clickedRef.current = true
unsetClickedRef.current = setTimeout(() => {
clickedRef.current = false
}, 1000)
if (activeState !== id) {
setActiveState(id)
}
}
useEffect(
() => {
findActiveIndex()
return () => {
if (unsetClickedRef.current) {
clearTimeout(unsetClickedRef.current)
}
}
},
[]
)
const itemLink = (item: TableOfContentsItem): React.ReactElement => (
<Link
display="block"
color={activeState === item.id ? 'textPrimary' : 'textSecondary'}
href={`#${item.id}`}
underline="none"
onClick={handleClick(item.id)}
className={clsx(
classes.item,
activeState === item.id ? classes.active : undefined
)}
>
<span dangerouslySetInnerHTML={{ __html: item.title }} />
</Link>
)
return (
<nav className={classes.root}>
{items && items.length > 0 ? (
<>
<Typography gutterBottom className={classes.contents}>
Contents
</Typography>
<Typography component="ul" className={classes.ul}>
{items.map((item) => (
<li key={item.id}>{itemLink(item)}</li>
))}
</Typography>
</>
) : null}
</nav>
)
}

View file

@ -0,0 +1,48 @@
html, body, #___gatsby, #gatsby-focus-wrapper {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden
}
html:not(.touch) *:not(.default-scroll) {
scrollbar-color: #7f7f7f transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
margin-left: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #7f7f7f;
border-radius: 6px;
}
}
// 80px offset for scroll anchors
section, .is-anchor {
margin-top: -80px;
padding-top: 80px;
}
h2, h3, h4 {
word-break: break-all;
}
table {
overflow-x: auto;
display: block!important;
white-space: nowrap;
}
table tbody {
display: table;
width: 100%;
}

View file

@ -0,0 +1,41 @@
import Fuse from 'fuse.js'
import { ChangeEvent, useCallback, useMemo, useState } from 'react'
import { debounce } from '@material-ui/core'
export function useFuse<T>(
items: T[],
options: Fuse.IFuseOptions<T>,
searchOptions: Fuse.FuseSearchOptions,
customFilter?: (it: T) => boolean
) {
const [query, updateQuery] = useState('')
const fuse = useMemo(() => new Fuse(items, options), [items, options])
const hits = useMemo(() => {
if (!query) return []
let res = fuse.search(query, searchOptions)
if (customFilter) res = res.filter((it) => customFilter(it.item))
return res
}, [
fuse,
query,
options,
searchOptions,
customFilter
])
const setQuery = useCallback(debounce(updateQuery, 100), [])
const onSearch = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),
[]
)
return {
hits,
onSearch,
query,
setQuery
}
}

View file

@ -0,0 +1,14 @@
import { useState } from 'react'
export function useLocalState<T>(key: string, init: T): [T, (val: T) => void] {
const local = typeof localStorage !== 'undefined' ? localStorage[key] : undefined
const [item, setItem] = useState<T>(local ? JSON.parse(local) : init)
return [
item,
(val: T) => {
setItem(val)
localStorage[key] = JSON.stringify(val)
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,210 @@
import React, { useEffect, useState } from 'react'
import {
AppBar,
Button,
createMuiTheme, createStyles,
IconButton,
List,
ListItem,
ListItemText, makeStyles,
MuiThemeProvider,
SwipeableDrawer,
Toolbar,
Tooltip,
useMediaQuery,
} from '@material-ui/core'
import { Link } from 'gatsby'
import { Spacer } from './components/spacer'
import { Helmet } from 'react-helmet'
import NightsStayIcon from '@material-ui/icons/NightsStay'
import Brightness7Icon from '@material-ui/icons/Brightness7'
import MenuIcon from '@material-ui/icons/Menu'
import './global.scss'
import { GlobalSearchField } from './components/global-search-field'
import blue from '@material-ui/core/colors/blue'
import { isTouchDevice } from './utils'
const pages = [
{
path: '/',
name: 'About',
regex: /^(?:\/tl|\/)\/?$/
},
{
path: '/types',
name: 'Types',
regex: /^(?:\/tl)?(?:\/mtproto)?\/(class|union|types)(\/|$)/,
},
{
path: '/methods',
name: 'Methods',
regex: /^(?:\/tl)?(?:\/mtproto)?\/methods?(\/|$)/,
},
// {
// path: '/history',
// name: 'History',
// regex: /^\/history(\/|$)/,
// },
]
const drawerWidth = 240
const useStyles = makeStyles((t) => createStyles({
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
},
drawerItem: {
padding: t.spacing(2, 4),
fontSize: 18
}
}))
function MobileNavigation({ path }: { path: string }) {
const [drawer, setDrawer] = useState(false)
const classes = useStyles()
return (
<>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={() => setDrawer(true)}
edge="start"
>
<MenuIcon />
</IconButton>
<SwipeableDrawer
onClose={() => setDrawer(false)}
onOpen={() => setDrawer(true)}
open={drawer}
className={classes.drawer}
classes={{
paper: classes.drawerPaper,
}}
>
<List>
{pages.map((page) => (
<ListItem
button
component={Link}
to={page.path}
selected={
page.regex
? !!path.match(page.regex)
: path === page.path
}
className={classes.drawerItem}
key={page.name}
>
<ListItemText primary={page.name} />
</ListItem>
))}
</List>
</SwipeableDrawer>
</>
)
}
function DesktopNavigation({ path }: { path: string }) {
return (
<>
{pages.map((page) => (
<span
style={{
color: (
page.regex
? path.match(page.regex)
: path === page.path
)
? '#fff'
: '#ccc',
}}
key={page.path}
>
<Button color="inherit" component={Link} to={page.path}>
{page.name}
</Button>
</span>
))}
<Spacer />
</>
)
}
export default function ({
children,
location,
}: {
children: NonNullable<React.ReactNode>
location: any
}): React.ReactElement {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const path: string = location.pathname
useEffect(() => {
if (isTouchDevice()) document.documentElement.classList.add('touch')
}, [])
const muiTheme = createMuiTheme({
palette: {
type: theme,
primary:
theme === 'dark'
? {
main: blue[300],
}
: undefined,
secondary: {
main: blue[800],
},
},
})
const isDesktop = useMediaQuery(muiTheme.breakpoints.up('sm'))
return (
<>
<Helmet
titleTemplate="%s | TL Reference"
defaultTitle="TL Reference"
/>
<MuiThemeProvider theme={muiTheme}>
<>
<AppBar position="static" color="secondary">
<Toolbar>
{isDesktop ? (
<DesktopNavigation path={path} />
) : (
<MobileNavigation path={path} />
)}
<GlobalSearchField isMobile={!isDesktop} />
<Tooltip title="Toggle dark theme">
<IconButton
color="inherit"
onClick={() =>
setTheme(
theme === 'dark' ? 'light' : 'dark'
)
}
>
{theme === 'light' ? (
<NightsStayIcon />
) : (
<Brightness7Icon />
)}
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
{children}
</>
</MuiThemeProvider>
</>
)
}

View file

@ -0,0 +1,27 @@
import * as React from 'react'
import { Typography } from '@material-ui/core'
import { Page, usePageStyles } from '../components/page'
import { Helmet } from 'react-helmet'
const NotFoundPage = () => {
const classes = usePageStyles()
return (
<Page>
<Helmet>
<title>404 Not found</title>
<meta name="robots" content="noindex" />
</Helmet>
<div className={classes.heading1}>
<Typography variant="h3" id="tl-reference">
404
</Typography>
</div>
<Typography variant="body1" className={classes.paragraph}>
This page does not exist
</Typography>
</Page>
)
}
export default NotFoundPage

View file

@ -0,0 +1,20 @@
import React from 'react'
import { Page, usePageStyles } from '../components/page'
import { Typography } from '@material-ui/core'
export default function HistoryPage() {
const classes = usePageStyles()
return (
<Page>
<div className={classes.heading1}>
<Typography variant="h3" id="tl-reference">
History
</Typography>
</div>
<Typography variant="body1" className={classes.paragraph}>
This page is currently under construction
</Typography>
</Page>
)
}

View file

@ -0,0 +1,367 @@
import React from 'react'
import { Link as MuiLink, Typography } from '@material-ui/core'
import { Page, usePageStyles } from '../components/page'
import { graphql, Link } from 'gatsby'
interface Data {
mtClasses: { totalCount: number }
classes: { totalCount: number }
mtMethods: { totalCount: number }
methods: { totalCount: number }
mtUnions: { totalCount: number }
unions: { totalCount: number }
clsWithDesc: { totalCount: number }
clsWithoutDesc: { totalCount: number }
argWithDesc: {
totalCount: number
nodes: { arguments: { description: string | null }[] }[]
}
argWithoutDesc: {
totalCount: number
nodes: { arguments: { description: string | null }[] }[]
}
updated: {
nodes: [
{
layer: number
source: {
date: string
commit: string
file: string
}
}
]
}
}
function countMissingDescriptionArguments(
item: Data['argWithDesc'],
eqNull: boolean
) {
let count = 0
item.nodes.forEach((node) =>
node.arguments?.forEach((arg) => {
if (eqNull ? arg.description === null : arg.description !== null)
count += 1
})
)
item.totalCount = count
}
export default function IndexPage({ data }: { data: Data }) {
const classes = usePageStyles()
countMissingDescriptionArguments(data.argWithoutDesc, true)
countMissingDescriptionArguments(data.argWithDesc, false)
return (
<Page
toc={[
{ id: 'tl-reference', title: 'TL Reference' },
{ id: 'types', title: 'Types' },
{ id: 'core-types', title: 'Core types' },
{ id: 'statistics', title: 'Statistics' },
]}
>
<div className={classes.heading1}>
<Typography variant="h3" id="tl-reference">
TL Reference
</Typography>
<Typography variant="body2">
layer {data.updated.nodes[0].layer} / updated{' '}
{data.updated.nodes[0].source.date}
</Typography>
</div>
<Typography variant="body1" className={classes.paragraph}>
This web application allows easily browsing through myriads of
TL objects and reading through their documentation. Unlike{' '}
<MuiLink href="//core.telegram.org/schema">
official documentation
</MuiLink>
, this app has simpler structure, search and nice interface.
</Typography>
<Typography variant="body1" className={classes.paragraph}>
Even though this reference is intended to be used with{' '}
<MuiLink href="//github.com/teidesu/mtcute">MTCute</MuiLink>{' '}
library, the objects are common to any other MTProto library.
The key difference is that MTCute (and this reference) use{' '}
<code>camelCase</code> for arguments, while the original schema
and some other libraries use <code>snake_case</code>.
</Typography>
<Typography variant="h4" id="types" className={classes.heading}>
Types
</Typography>
<Typography variant="body1" className={classes.paragraph}>
In TL, there are 3 main groups of types: <i>Classes</i>,{' '}
<i>Methods</i> and Unions (officially they are called{' '}
<i>constructors</i>, <i>methods</i> and <i>types</i>{' '}
respectively).
</Typography>
<Typography variant="body1" className={classes.paragraph}>
<i>Classes</i> and <i>Methods</i> are simply typed objects, that
contain some data. The only difference is that Methods are used
in RPC calls (i.e. they are sent to the server), and Classes are
used inside methods, or sent by the server back (either as an
RPC result, or as an update).
</Typography>
<Typography variant="body1" className={classes.paragraph}>
<i>Union</i> is a type that combines multiple <i>Classes</i> in
one type. In some languages, this can be represented as an
abstract class. <i>Unions</i> are sent by Telegram in response
to RPC results, as well as they are used as arguments for other{' '}
<i>Classes</i> or <i>Methods</i>.
</Typography>
<Typography variant="body1" className={classes.paragraph}>
In TL, every single <i>Class</i> is a part of exactly one{' '}
<i>Union</i>, and every <i>Union</i> contains at least one{' '}
<i>Class</i>.
</Typography>
<Typography variant="body1" className={classes.paragraph}>
In MTCute, all types are exposed as a namespace <code>tl</code>{' '}
of package <code>@mtcute/tl</code>. By design, we use immutable
plain objects with type discriminator to represent{' '}
<i>Classes</i> and <i>Methods</i>, and TypeScript unions to
represent <i>Unions</i>.<br />
To differentiate between different groups of types, we use
different naming for each of them:
</Typography>
<Typography
variant="body1"
className={classes.paragraph}
component="ul"
>
<li>
<i>Classes</i> are prefixed with <code>Raw</code> (e.g.{' '}
<code>tl.RawMessage</code>)
</li>
<li>
Additionally, <i>Methods</i> are postfixed with{' '}
<code>Request</code> and (e.g.{' '}
<code>tl.RawGetMessageRequest</code>)
</li>
<li>
Finally, <i>Unions</i> are simply prefixed with{' '}
<code>Type</code> (e.g. <code>tl.TypeUser</code>)
</li>
</Typography>
<Typography
variant="h4"
id="core-types"
className={classes.heading}
>
Core types
</Typography>
<Typography variant="body1" className={classes.paragraph}>
Core types are basic built-in types that are used in TL schema.
Quick reference:
</Typography>
<Typography
variant="body1"
className={classes.paragraph}
component="ul"
>
<li>
<code>number</code>: 32-bit signed integer
</li>
<li>
<code>Long</code>: 64-bit signed integer
</li>
<li>
<code>Int128</code>: 128-bit signed integer (only used for
MTProto)
</li>
<li>
<code>Int256</code>: 256-bit signed integer (only used for
MTProto)
</li>
<li>
<code>Double</code>: 64-bit floating point value
</li>
<li>
<code>string</code>: UTF-16 string (strings in JS are also
UTF-16)
</li>
<li>
<code>Buffer</code>: Byte array of a known size
</li>
<li>
<code>boolean</code>: One-byte boolean value (true/false)
</li>
<li>
<code>true</code>: Zero-size <code>true</code> value, used
for TL flags
</li>
<li>
<code>any</code>: Any other TL object (usually another
method)
</li>
<li>
<code>T[]</code>: Array of <code>T</code>
</li>
<li>
<code>TlFlags</code>: 32-bit signed value representing
object's TL flags
</li>
</Typography>
<Typography
variant="h4"
className={classes.heading}
id="statistics"
>
Statistics
</Typography>
<Typography
variant="body1"
className={classes.paragraph}
component="ul"
>
<li>
Generated from layer <b>{data.updated.nodes[0].layer}</b>{' '}
(last updated <b>{data.updated.nodes[0].source.date}</b>,
commit{' '}
<MuiLink
href={`https://github.com/telegramdesktop/tdesktop/blob/${data.updated.nodes[0].source.commit}/${data.updated.nodes[0].source.file}`}
target="_blank"
>
{data.updated.nodes[0].source.commit.substr(0, 7)}
</MuiLink>
)
</li>
<li>
Current schema contains{' '}
<b>
{data.methods.totalCount +
data.classes.totalCount +
data.unions.totalCount}
</b>{' '}
types (+{' '}
<b>
{data.mtClasses.totalCount +
data.mtMethods.totalCount +
data.mtUnions.totalCount}
</b>{' '}
for MTProto)
</li>
<li>
Current schema contains <b>{data.classes.totalCount}</b>{' '}
classes (+ <b>{data.mtClasses.totalCount}</b> for MTProto)
</li>
<li>
Current schema contains <b>{data.methods.totalCount}</b>{' '}
methods (+ <b>{data.mtMethods.totalCount}</b> for MTProto)
</li>
<li>
Current schema contains <b>{data.unions.totalCount}</b>{' '}
unions (+ <b>{data.mtUnions.totalCount}</b> for MTProto)
</li>
<li>
Description coverage:{' '}
{(function () {
const totalWith =
data.argWithDesc.totalCount +
data.clsWithDesc.totalCount
const totalWithout =
data.argWithoutDesc.totalCount +
data.clsWithoutDesc.totalCount
const total = totalWith + totalWithout
return (
<>
<b>
{Math.round((totalWith / total) * 10000) /
100}
%
</b>{' '}
(out of {total} items, {totalWithout}{' '}
<MuiLink component={Link} to="/no-description">
don't have description
</MuiLink>{' '}
- that is {data.clsWithoutDesc.totalCount} types
and {data.argWithoutDesc.totalCount} arguments)
</>
)
})()}
</li>
</Typography>
</Page>
)
}
export const query = graphql`
query {
mtClasses: allTlObject(
filter: { type: { eq: "class" }, prefix: { eq: "mtproto/" } }
) {
totalCount
}
classes: allTlObject(
filter: { type: { eq: "class" }, prefix: { ne: "mtproto/" } }
) {
totalCount
}
mtMethods: allTlObject(
filter: { type: { eq: "method" }, prefix: { eq: "mtproto/" } }
) {
totalCount
}
methods: allTlObject(
filter: { type: { eq: "method" }, prefix: { ne: "mtproto/" } }
) {
totalCount
}
mtUnions: allTlObject(
filter: { type: { eq: "union" }, prefix: { eq: "mtproto/" } }
) {
totalCount
}
unions: allTlObject(
filter: { type: { eq: "union" }, prefix: { ne: "mtproto/" } }
) {
totalCount
}
updated: allHistoryJson(
sort: { fields: source___date, order: DESC }
filter: { layer: { ne: null } }
limit: 1
) {
nodes {
layer
source {
date(formatString: "DD-MM-YYYY")
commit
file
}
}
}
clsWithDesc: allTlObject(filter: { description: { ne: null } }) {
totalCount
}
clsWithoutDesc: allTlObject(filter: { description: { eq: null } }) {
totalCount
}
argWithDesc: allTlObject(
filter: { arguments: { elemMatch: { description: { ne: null } } } }
) {
totalCount
nodes {
arguments {
description
}
}
}
argWithoutDesc: allTlObject(
filter: { arguments: { elemMatch: { description: { eq: null } } } }
) {
totalCount
nodes {
arguments {
description
}
}
}
}
`

View file

@ -0,0 +1,178 @@
import { graphql } from 'gatsby'
import React from 'react'
import { ExtendedTlObject } from '../types'
import { Page, Section, usePageStyles } from '../components/page'
import {
Link as MuiLink,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
} from '@material-ui/core'
import { LinkToTl } from '../components/objects/link-to-tl'
interface Data {
classes: { nodes: ExtendedTlObject[] }
arguments: { nodes: ExtendedTlObject[] }
}
export default function NoDescriptionPage({ data }: { data: Data }) {
const classes = usePageStyles()
// i dont care
const args: any[] = []
data.arguments.nodes.forEach((node) => {
if (node.arguments) {
node.arguments.forEach((arg) => {
if (arg.description === null) {
;(arg as any).node = node
args.push(arg)
}
})
}
})
return (
<Page
toc={[
{ id: 'types', title: 'Types' },
{ id: 'arguments', title: 'Arguments' },
]}
>
<div className={classes.heading1}>
<Typography variant="h3" id="tl-reference">
No description
</Typography>
<Typography variant="body2">
{data.classes.nodes.length} types, {args.length} arguments
</Typography>
</div>
<Typography variant="body1" className={classes.paragraph}>
This page lists all items (types and their arguments) from the
schema that currently do not have a description, neither
official nor unofficial. You can improve this reference by
adding description to missing items in{' '}
<MuiLink href="https://github.com/teidesu/mtcute/blob/master/packages/tl/descriptions.yaml">
descriptions.yaml
</MuiLink>
.
</Typography>
<Section id="types" title="Types">
<Table>
<TableHead>
<TableRow>
<TableCell>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell>
<MuiLink href="https://github.com/teidesu/mtcute/blob/master/packages/tl/descriptions.yaml">
descriptions.yaml
</MuiLink>{' '}
key
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.classes.nodes.map((node) => (
<TableRow key={node.id}>
<TableCell>
{node.type === 'method'
? 'Method'
: node.type === 'union'
? 'Union'
: 'Class'}
</TableCell>
<TableCell>{LinkToTl(node)}</TableCell>
<TableCell>
{(node.type === 'method' ? 'm_' : 'o_') +
(node.prefix === 'mtproto/'
? 'mt_'
: '') +
(node.type === 'union'
? node.name
: node.underscore)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Section>
<Section id="arguments" title="Arguments">
<Table>
<TableHead>
<TableRow>
<TableCell>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell>
<MuiLink href="https://github.com/teidesu/mtcute/blob/master/packages/tl/descriptions.yaml">
descriptions.yaml
</MuiLink>{' '}
key
</TableCell>
<TableCell>Argument</TableCell>
</TableRow>
</TableHead>
<TableBody>
{args.map((arg) => (
<TableRow key={arg.node.id + arg.name}>
<TableCell>
{arg.node.type === 'method'
? 'Method'
: arg.node.type === 'union'
? 'Union'
: 'Class'}
</TableCell>
<TableCell>{LinkToTl(arg.node)}</TableCell>
<TableCell>
{(arg.node.type === 'method'
? 'm_'
: 'o_') +
(arg.node.prefix === 'mtproto/'
? 'mt_'
: '') +
(arg.node.type === 'union'
? arg.node.name
: arg.node.underscore)}
</TableCell>
<TableCell>
<code>{arg.name}</code>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Section>
</Page>
)
}
export const query = graphql`
query {
classes: allTlObject(filter: { description: { eq: null } }) {
nodes {
id
prefix
type
name
underscore
}
}
arguments: allTlObject(
filter: { arguments: { elemMatch: { description: { eq: null } } } }
) {
nodes {
id
prefix
type
name
underscore
arguments {
name
type
description
}
}
}
}
`

View file

@ -0,0 +1,486 @@
import React, { useMemo } from 'react'
import { graphql } from 'gatsby'
import { ExtendedTlObject } from '../types'
import {
Description,
Page,
Section,
SectionWithList,
usePageStyles,
} from '../components/page'
import {
Breadcrumbs,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from '@material-ui/core'
import {
createStyles,
Link as MuiLink,
makeStyles,
Typography,
} from '@material-ui/core'
import { Link } from 'gatsby'
import { LinkToTl } from '../components/objects/link-to-tl'
import { TableOfContentsItem } from '../components/table-of-contents'
import { Helmet } from 'react-helmet'
interface GraphqlResult {
self: ExtendedTlObject
parent: ExtendedTlObject
children: { nodes: ExtendedTlObject[] }
usageMethods: { nodes: ExtendedTlObject[] }
usageTypes: { nodes: ExtendedTlObject[] }
}
const useStyles = makeStyles((theme) =>
createStyles({
description: {
marginBottom: theme.spacing(2),
fontSize: 16,
},
table: {
'& th, & td': {
fontSize: 15,
},
},
mono: {
fontFamily: 'Fira Mono, Consolas, monospace',
},
// theme ported from one dark
code: {
fontFamily: 'Fira Mono, Consolas, monospace',
background: '#282c34',
color: '#bbbbbb',
fontSize: 16,
borderRadius: 4,
overflowX: 'auto',
padding: 8,
},
keyword: {
fontStyle: 'italic',
color: '#c678dd',
},
identifier: {
color: '#e5c07b',
},
property: {
color: '#e06c75',
},
comment: {
color: '#5c6370',
},
string: {
color: '#98c379',
},
})
)
function useToc(obj: ExtendedTlObject): TableOfContentsItem[] {
return useMemo(() => {
const ret = [{ id: 'title', title: obj.prefix + obj.name }]
if (obj.type !== 'union') {
ret.push({ id: 'parameters', title: 'Parameters' })
} else {
ret.push({ id: 'subtypes', title: 'Subtypes' })
ret.push({ id: 'usage', title: 'Usage' })
}
if (obj.type === 'method' && obj.throws) {
ret.push({ id: 'throws', title: 'Throws' })
}
ret.push({ id: 'typescript', title: 'TypeScript' })
return ret
}, [obj])
}
export default function TlObject({ data }: { data: GraphqlResult }) {
const pageClasses = usePageStyles()
const classes = useStyles()
const obj = data.self
const toc = useToc(obj)
const keyword = (s: string) =>
`<span class='${classes.keyword}'>${s}</span>`
const identifier = (s: string) =>
`<span class='${classes.identifier}'>${s}</span>`
const property = (s: string) =>
`<span class='${classes.property}'>${s}</span>`
const comment = (s: string) =>
`<span class='${classes.comment}'>${s}</span>`
const _string = (s: string) => `<span class='${classes.string}'>${s}</span>`
const typeName = (s: string): string => {
if (
s === 'string' ||
s === 'number' ||
s === 'boolean' ||
s === 'true'
) {
return keyword(s)
}
if (s.substr(s.length - 2) === '[]')
return typeName(s.substr(0, s.length - 2)) + '[]'
return s.split('.').map(identifier).join('.')
}
const code = (s: string) => {
return (
<pre
className={classes.code}
dangerouslySetInnerHTML={{ __html: s }}
/>
)
}
return (
<Page toc={toc}>
<Helmet>
<title>
{obj.prefix}
{obj.name}
</title>
<meta
name="description"
content={
obj.description ||
obj.prefix + obj.name + " currently doesn't have a description."
}
/>
</Helmet>
<div className={pageClasses.heading0}>
<Breadcrumbs>
<MuiLink
component={Link}
to={`/${obj.prefix}${
obj.type === 'method' ? 'methods' : 'types'
}`}
>
{obj.prefix}
{obj.type === 'method' ? 'Methods' : 'Types'}
</MuiLink>
{obj.namespace !== '$root' && (
<MuiLink
component={Link}
to={`/${obj.prefix}${
obj.type === 'method' ? 'methods' : 'types'
}/${obj.namespace}`}
>
{obj.prefix}
{obj.namespace}
</MuiLink>
)}
<Typography color="textPrimary">{obj.name}</Typography>
</Breadcrumbs>
<Typography variant="h3" id="title">
{obj.prefix}
{obj.name}
</Typography>
<Typography variant="body2">
{obj.type === 'class' ? (
<>
constructor ID 0x{obj.tlId} / belongs to union{' '}
{LinkToTl(data.parent)}
</>
) : obj.type === 'union' ? (
<>
has{' '}
<MuiLink href="#subtypes">
{data.children.nodes.length} sub-types
</MuiLink>{' '}
and{' '}
<MuiLink href="#usage">
{data.usageTypes.nodes.length +
data.usageMethods.nodes.length}{' '}
usages
</MuiLink>
</>
) : (
obj.returns && (
<>
constructor ID 0x{obj.tlId} / returns{' '}
{LinkToTl(obj.prefix, 'union', obj.returns)}
{obj.available &&
' / available for ' +
(obj.available === 'both'
? 'both users and bots'
: obj.available + 's only')}
</>
)
)}
</Typography>
</div>
<Description
description={obj.description}
className={classes.description}
/>
{obj.type !== 'union' && (
<Section id="parameters" title="Parameters">
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Type</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{obj.arguments.map((arg) => (
<TableRow key={arg.name}>
<TableCell>
<code>{arg.name}</code>
</TableCell>
<TableCell className={classes.mono}>
{LinkToTl(arg.type)}
{arg.optional ? '?' : ''}
</TableCell>
<Description
description={arg.description}
component={TableCell}
/>
</TableRow>
))}
</TableBody>
</Table>
</Section>
)}
{obj.type === 'union' && (
<>
<SectionWithList
id="subtypes"
title="Subtypes"
nodes={data.children.nodes}
>
{obj.prefix}
{obj.name} can be represented with{' '}
{obj.subtypes.length > 1
? `one of ${obj.subtypes.length} classes`
: 'only one class'}
:
</SectionWithList>
<Section id="usage" title="Usage">
{data.usageMethods.nodes.length > 0 && (
<SectionWithList nodes={data.usageMethods.nodes}>
{obj.prefix}
{obj.name} is returned by{' '}
{data.usageMethods.nodes.length > 1
? `${data.usageMethods.nodes.length} methods`
: 'only one method'}
:
</SectionWithList>
)}
{data.usageTypes.nodes.length > 0 && (
<SectionWithList nodes={data.usageTypes.nodes}>
{obj.prefix}
{obj.name} is used in{' '}
{data.usageTypes.nodes.length > 1
? `${data.usageTypes.nodes.length} types`
: 'only one type'}
:
</SectionWithList>
)}
{data.usageMethods.nodes.length === 0 &&
data.usageTypes.nodes.length === 0 && (
<Typography color="textSecondary">
This union is never used :(
</Typography>
)}
</Section>
</>
)}
{obj.throws && (
<Section id="throws" title="Throws">
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Code</TableCell>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{obj.throws.map((err) => (
<TableRow key={err.name}>
<TableCell>
<code>{err.code}</code>
</TableCell>
<TableCell>
<code>{err.name}</code>
</TableCell>
<Description
description={err.description}
component={TableCell}
/>
</TableRow>
))}
</TableBody>
</Table>
</Section>
)}
<Typography
variant="h4"
id="typescript"
className={pageClasses.heading}
>
TypeScript declaration
</Typography>
{/* this is a mess, but who cares */}
{code(
obj.type === 'union'
? `${keyword('export type')} ${identifier(obj.ts)} =` +
data.children.nodes
.map(
(it) =>
`\n | ${typeName(
'tl.' +
(it.namespace === '$root'
? it.prefix === 'mtproto/'
? 'mtproto.'
: ''
: it.namespace + '.') +
it.ts
)}`
)
.join('')
: `${keyword('export interface')} ${identifier(obj.ts)} {` +
`\n ${property('_')}: ${_string(
`'${obj.prefix === 'mtproto/' ? 'mt_' : ''}${
obj.name
}'`
)}` +
obj.arguments
.map((arg) =>
arg.type === '$FlagsBitField'
? comment(
'\n // ' +
arg.name +
': TlFlags // handled automatically'
)
: `\n ${property(arg.name)}${
arg.optional ? '?' : ''
}: ${typeName(arg.ts)}${
arg.predicate
? ' ' +
comment(
'// present if ' +
arg.predicate
)
: ''
}`
)
.join('') +
'\n}'
)}
</Page>
)
}
export const query = graphql`
query(
$prefix: String!
$type: String!
$name: String!
$hasSubtypes: Boolean!
$subtypes: [String]
) {
self: tlObject(
prefix: { eq: $prefix }
type: { eq: $type }
name: { eq: $name }
) {
tlId
ts
prefix
type
name
description
namespace
returns
available
arguments {
name
ts
type
description
optional
predicate
}
subtypes
throws {
code
description
name
}
}
parent: tlObject(
prefix: { eq: $prefix }
type: { eq: "union" }
subtypes: { eq: $name }
) {
prefix
name
type
description
subtypes
}
children: allTlObject(
filter: {
prefix: { eq: $prefix }
type: { eq: "class" }
name: { in: $subtypes }
}
) @include(if: $hasSubtypes) {
nodes {
ts
id
namespace
prefix
name
type
description
}
}
usageMethods: allTlObject(
filter: {
prefix: { eq: $prefix }
type: { eq: "method" }
returns: { eq: $name }
}
) @include(if: $hasSubtypes) {
nodes {
id
prefix
type
name
description
}
}
usageTypes: allTlObject(
filter: {
prefix: { eq: $prefix }
arguments: { elemMatch: { type: { eq: $name } } }
}
) {
nodes {
id
prefix
type
name
description
}
}
}
`

View file

@ -0,0 +1,279 @@
import { graphql, Link } from 'gatsby'
import React, { useMemo } from 'react'
import { ExtendedTlObject } from '../types'
import {
Page,
Section,
SectionWithList,
usePageStyles,
} from '../components/page'
import {
Breadcrumbs,
createStyles,
Link as MuiLink,
makeStyles,
Typography,
} from '@material-ui/core'
import { TableOfContentsItem } from '../components/table-of-contents'
import { Helmet } from 'react-helmet'
interface Data {
classes: { nodes: ExtendedTlObject[] }
unions: { nodes: ExtendedTlObject[] }
methods: { nodes: ExtendedTlObject[] }
other: { group: { fieldValue: string }[] }
}
interface Context {
ns: string
prefix: string
type: string
}
const useStyles = makeStyles((theme) =>
createStyles({
namespace: {
display: 'inline-block',
margin: theme.spacing(0, 1),
fontSize: 16,
},
})
)
function useToc(data: Data, ctx: Context): TableOfContentsItem[] {
return useMemo(() => {
const ret = [{ id: 'namespaces', title: 'Namespaces' }]
if (ctx.type === 'types') {
ret.push({ id: 'classes', title: 'Classes' })
ret.push({ id: 'unions', title: 'Unions' })
} else {
ret.push({ id: 'methods', title: 'Methods' })
}
return ret
}, [data, ctx])
}
export default function TlTypesList({
data,
pageContext: ctx,
}: {
data: Data
pageContext: Context
}) {
const pageClasses = usePageStyles()
const classes = useStyles()
const toc = useToc(data, ctx)
const title = `${ctx.type === 'methods' ? 'Methods' : 'Types'}
${
ctx.ns === '$root' && ctx.prefix === ''
? ''
: ctx.ns === '$root'
? `in ${ctx.prefix.slice(0, ctx.prefix.length - 1)}`
: ` in ${ctx.prefix}${ctx.ns}`
}`
const plural = (val: number, singular: string, postfix = 's') =>
val + ' ' + (val === 1 ? singular : singular + postfix)
let description = ''
{
if (ctx.prefix === 'mtproto/') description = 'MTProto '
if (ctx.ns === '$root') {
description += 'TL Schema'
} else {
description += 'Namespace ' + ctx.ns
}
description += ' contains '
if (ctx.ns === '$root' && data.other.group.length) {
description += plural(data.other.group.length, 'namespace') + ', '
}
if (ctx.type === 'methods') {
description +=
plural(data.methods.nodes.length, 'method') +
', including: ' +
data.methods.nodes
.slice(0, 3)
.map((i) => i.name)
.join(', ')
if (data.methods.nodes.length > 3) description += ' and others.'
} else {
description +=
plural(data.classes.nodes.length, 'class', 'es') +
' and ' +
plural(data.unions.nodes.length, 'union') +
', including: ' +
data.classes.nodes
.slice(0, 3)
.map((i) => i.name)
.join(', ')
if (data.classes.nodes.length > 3) description += ' and others.'
}
}
return (
<Page toc={toc}>
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
</Helmet>
<div className={pageClasses.heading0}>
<Breadcrumbs>
{ctx.ns === '$root' ? (
<Typography color="textPrimary">
{ctx.prefix}{ctx.type === 'methods' ? 'Methods' : 'Types'}
</Typography>
) : (
[
<MuiLink
component={Link}
to={`/${ctx.prefix}${ctx.type}`}
key="type"
>
{ctx.prefix}{ctx.type === 'methods' ? 'Methods' : 'Types'}
</MuiLink>,
<Typography color="textPrimary" key="namespace">
{ctx.ns}
</Typography>,
]
)}
</Breadcrumbs>
<Typography variant="h3" id="title">
{title}
</Typography>
<Typography variant="body2">
{ctx.ns === '$root' &&
`has ${plural(
data.other.group.length,
'namespace'
)} / `}
{ctx.type === 'methods'
? `has ${plural(data.methods.nodes.length, 'method')}`
: `has ${plural(
data.classes.nodes.length,
'class',
'es'
)}` +
` and ${plural(data.unions.nodes.length, 'union')}`}
</Typography>
</div>
{data.other.group.length && (
<Section id="namespaces" title="Namespaces">
{data.other.group.map(({ fieldValue: it }) =>
it === ctx.ns ? (
<Typography
color="textPrimary"
className={classes.namespace}
key={it}
>
{ctx.ns === '$root' ? 'root' : ctx.ns}
</Typography>
) : (
<MuiLink
component={Link}
to={`/${ctx.prefix}${ctx.type}${
it === '$root' ? '' : '/' + it
}`}
className={classes.namespace}
key={it}
>
{it === '$root' ? 'root' : it}
</MuiLink>
)
)}
</Section>
)}
{ctx.type === 'types' && (
<>
<SectionWithList
id="classes"
title="Classes"
nodes={data.classes.nodes}
/>
<SectionWithList
id="unions"
title="Unions"
nodes={data.unions.nodes}
/>
</>
)}
{ctx.type === 'methods' && (
<SectionWithList
id="methods"
title="Methods"
nodes={data.methods.nodes}
/>
)}
</Page>
)
}
export const query = graphql`
query(
$ns: String!
$prefix: String!
$isTypes: Boolean!
$isMethods: Boolean!
) {
classes: allTlObject(
filter: {
prefix: { eq: $prefix }
namespace: { eq: $ns }
type: { eq: "class" }
}
) @include(if: $isTypes) {
nodes {
id
prefix
type
name
description
}
}
unions: allTlObject(
filter: {
prefix: { eq: $prefix }
namespace: { eq: $ns }
type: { eq: "union" }
}
) @include(if: $isTypes) {
nodes {
id
prefix
type
name
description
}
}
methods: allTlObject(
filter: {
prefix: { eq: $prefix }
namespace: { eq: $ns }
type: { eq: "method" }
}
) @include(if: $isMethods) {
nodes {
id
prefix
type
name
description
}
}
other: allTlObject(filter: { prefix: { eq: $prefix } }) {
group(field: namespace) {
fieldValue
}
}
}
`

View file

@ -0,0 +1,33 @@
export interface ExtendedTlObject {
id: string
tlId: number
ts: string
prefix: string
available: 'user' | 'bot' | 'both'
type: 'union' | 'class' | 'method'
name: string
namespace: string
returns: string
underscore: string
description: string | null
arguments: {
ts: string
optional?: boolean
name: string
type: string
predicate: string
description: string | null
}[]
throws: {
name: string
code: string
description: string
}[]
subtypes: string[]
}
export interface GraphqlAllResponse<T> {
edges: {
node: T
}[]
}

View file

@ -0,0 +1,20 @@
export const isTouchDevice = function (): boolean {
// because windows touch support stinks
if (navigator.userAgent.match(/Windows NT/i)) return false;
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
const mq = function (query: string): boolean {
return window.matchMedia(query).matches
}
if ('ontouchstart' in window
|| (navigator.maxTouchPoints > 0)
|| (navigator.msMaxTouchPoints > 0)
|| (window as any).DocumentTouch && document instanceof (window as any).DocumentTouch) {
return true
}
const query = prefixes.map(i => `(${i}touch-enabled)`).join(',')
return mq(query)
}

View file

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
},
"include": "src"
}

View file

@ -84,7 +84,7 @@ function getJSType(typ, argName) {
return normalizeGenerics(typ)
}
async function convertTlToJson(tlText, tlType) {
async function convertTlToJson(tlText, tlType, silent = false) {
let lines = tlText.split('\n')
let pos = 0
let line = lines[0].trim()
@ -130,7 +130,7 @@ async function convertTlToJson(tlText, tlType) {
state.type = 'class'
return nextLine()
}
process.stdout.write(
if (!silent) process.stdout.write(
`[${pad(pos)}/${lines.length}] Processing ${tlType}.tl..\r`
)
}
@ -148,7 +148,7 @@ async function convertTlToJson(tlText, tlType) {
return ret[name]
}
process.stdout.write(
if (!silent) process.stdout.write(
`[${pad(pos)}/${lines.length}] Processing ${tlType}.tl..\r`
)
@ -294,7 +294,7 @@ async function convertTlToJson(tlText, tlType) {
})
})
console.log(`[${lines.length}/${lines.length}] Processed ${tlType}.tl`)
if (!silent) console.log(`[${lines.length}/${lines.length}] Processed ${tlType}.tl`)
return ret
}
@ -522,4 +522,10 @@ async function main() {
await fs.promises.writeFile(path.join(__dirname, '../README.md'), readmeMd)
}
main().catch(console.error)
module.exports = {
convertTlToJson
}
if (require.main === module) {
main().catch(console.error)
}

14
scripts/deploy-docs.bat Normal file
View file

@ -0,0 +1,14 @@
@echo off
cd %~dp0\..\docs
echo mt.tei.su > CNAME
rem reset git repo
rd /s /q .git
git init
git add --all . > nul 2> nul
git commit -am deploy > nul 2> nul
git push -f https://github.com/teidesu/mtcute.git master:gh-pages
cd ../

9127
yarn.lock

File diff suppressed because it is too large Load diff