Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/components/Component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import StaticComponent, { ComponentState } from './StaticComponent';
import InteractiveComponent from './InteractiveComponent';

import {
ActionRowComponent,
ButtonComponent,
ContainerComponent,
EmbedStructure,
MediaComponent,
SectionComponent,
TextDisplayComponent,
} from "../types";

export type SupportedComponents =
| EmbedStructure
| ButtonComponent
| ActionRowComponent
| TextDisplayComponent
| SectionComponent
| MediaComponent
| ContainerComponent;

export enum ComponentType {
Static = 'Static',
Interactive = 'Interactive',
}

export type StateListener<T> = (newState: T) => void;
export type StateSetter<T> = (newState: Partial<T> | ((prevState: T) => T)) => void;

export default class Component {
private static componentRegistry = new Map<symbol, StaticComponent<SupportedComponents> | InteractiveComponent<SupportedComponents>>();
private static stateRegistry = new Map<symbol, ComponentState<SupportedComponents>>();

public static registerComponent<T extends SupportedComponents>(id: symbol, component: StaticComponent<T> | InteractiveComponent<T>) {
Component.componentRegistry.set(id, component);
}

public static initializeState<T extends SupportedComponents>(id: symbol, data: T) {
Component.stateRegistry.set(id, { data, listeners: new Set<StateListener<SupportedComponents>>() });
}

public static getCurrentData<T extends SupportedComponents>(id: symbol): T {
const state = Component.stateRegistry.get(id);
if (!state) throw new Error('Component state not found');

return state.data as T;
}

public static useState<T extends SupportedComponents>(component: InteractiveComponent<T>): [T, StateListener<T>] {
const state = Component.stateRegistry.get(component.id);

if (!state) throw new Error('Component state not found. Make sure the component was created properly.');

const setter: StateSetter<T> = (newStateOrUpdater) => {
const currentState = Component.stateRegistry.get(component.id)!;

const newData = typeof newStateOrUpdater === 'function'
? newStateOrUpdater(currentState.data as T)
: { ...currentState.data as T, ...newStateOrUpdater } as T;

currentState.data = newData as SupportedComponents;
currentState.listeners.forEach(listener => listener(newData as SupportedComponents));
};

return [state.data as T, setter];
}

public static createStaticComponent<T extends SupportedComponents>(data: T): StaticComponent<T> {
return new StaticComponent<T>(data);
}

public static createInteractiveComponent<T extends SupportedComponents>(data: T): InteractiveComponent<T> {
return new InteractiveComponent<T>(data);
}

}
Empty file.
Empty file.
4 changes: 3 additions & 1 deletion src/types/ActionRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MessageComponentType } from "./types";

import { ButtonComponent } from './Button';
import { TextInputComponent } from './TextInputField';
import { LabelComponent } from './LabelComponent';

import {
ChannelSelectMenuComponent,
Expand All @@ -42,7 +43,8 @@ export type ActionRowComponents =
| RoleSelectMenuComponent
| StringSelectMenuComponent
| MentionableSelectMenuComponent
| ChannelSelectMenuComponent;
| ChannelSelectMenuComponent
| LabelComponent;

/**
* @type ActionRowComponent
Expand Down
21 changes: 21 additions & 0 deletions src/types/LabelComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BaseComponent } from './Component';
import { MessageComponentType } from './types';
import { TextInputComponent } from "./TextInputField";

import {
ChannelSelectMenuComponent,
MentionableSelectMenuComponent,
RoleSelectMenuComponent,
UserSelectMenuComponent,
} from "./SelectMenu";

/**
* @type LabelComponent
* @description Label component container for select menus in modals
* @see https://discord.com/developers/docs/components/using-modal-components
*
**/
export type LabelComponent = BaseComponent & {
type: MessageComponentType.LABEL
components: Array<TextInputComponent | UserSelectMenuComponent | RoleSelectMenuComponent | MentionableSelectMenuComponent | ChannelSelectMenuComponent>
};
28 changes: 14 additions & 14 deletions src/types/Modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import { TextInputComponent } from './TextInputField';

/**
* @type RawModal
* @description The raw modal data as described by the Discord API
* @see https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-modal
*/
export type RawModal = {
custom_id: string
title: string
components: Array<{
type: 1
components: Array<TextInputComponent>
}>
import { LabelComponent } from "./LabelComponent";
/**
* @type RawModal
* @description The raw modal data as described by the Discord API
* @see https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-modal
*/
export type RawModal = {
custom_id: string
title: string
components: Array<{
type: 1
components: Array<LabelComponent>
}>
};
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './Container';
export * from './Embed';
export * from './File';
export * from './InteractibleComponent';
export * from './LabelComponent';
export * from './Media';
export * from './Modal';
export * from './Section';
Expand Down
41 changes: 21 additions & 20 deletions src/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
/**
* This file is part of Builders (https://github.com/FCAgreatgoals/builders).
*
* Copyright (C) 2025 SAS French Community Agency
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* This file is part of Builders (https://github.com/FCAgreatgoals/builders).
*
* Copyright (C) 2025 SAS French Community Agency
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

export enum MessageComponentType {
ACTION_ROW = 1,
BUTTON = 2,
Expand All @@ -32,7 +32,8 @@ export enum MessageComponentType {
MEDIA_GALERY = 12,
FILE = 13,
SEPARATOR = 14,
CONTAINER = 17
CONTAINER = 17,
LABEL = 18
}

/**
Expand Down