mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b6910d505 | ||
|
|
d87e31efaf | ||
|
|
6618a6baf4 | ||
|
|
22b8f5454b | ||
|
|
801bd3f106 |
@@ -1,24 +1,24 @@
|
||||
# Contributing to Fluxer
|
||||
|
||||
This policy applies to all issues, discussions, commits and pull requests.
|
||||
This policy applies to all commits and pull requests.
|
||||
|
||||
## Scope
|
||||
|
||||
To prevent spam, only approved contributors may submit pull requests.
|
||||
|
||||
To request approval, comment on an existing issue and ask to implement it. For work that extends beyond a defect fix, open a [discussion](https://github.com/orgs/fluxerapp/discussions) first.
|
||||
To request approval, comment on the [feedback.fluxer.com](https://feedback.fluxer.com) post you want to implement and ask to work on it. For work that extends beyond a defect fix, post a feature request there first.
|
||||
|
||||
Every pull request must:
|
||||
|
||||
- Target the repository's default branch.
|
||||
- Include a closing reference for each repository issue it resolves.
|
||||
- Link each feedback.fluxer.com post it resolves.
|
||||
- Receive approval from a maintainer before it is merged.
|
||||
|
||||
Place each closing reference on a separate line:
|
||||
Place each link on a separate line:
|
||||
|
||||
```text
|
||||
Closes #123
|
||||
Closes #456
|
||||
Resolves https://feedback.fluxer.com/p/123
|
||||
Resolves https://feedback.fluxer.com/p/456
|
||||
```
|
||||
|
||||
## Authorship
|
||||
@@ -78,11 +78,11 @@ Complete every section of the pull request template. Clearly describe:
|
||||
|
||||
## Reports and other contributions
|
||||
|
||||
Use the [bug report form](https://github.com/fluxerapp/fluxer/issues/new?template=bug-report.yaml) to report reproducible defects.
|
||||
Report bugs and request features at [feedback.fluxer.com](https://feedback.fluxer.com).
|
||||
|
||||
Report security vulnerabilities privately through the channels specified in the [security policy](https://github.com/fluxerapp/fluxer/blob/main/.github/SECURITY.md). Do not report vulnerabilities in public issues or discussions.
|
||||
Report security vulnerabilities privately through [fluxer.app/security](https://fluxer.app/security). Never post them publicly.
|
||||
|
||||
Use [discussions](https://github.com/orgs/fluxerapp/discussions) for feature proposals and self-hosting questions.
|
||||
Read the [operator documentation](https://fluxer.dev) for self-hosting questions.
|
||||
|
||||
Submit translations through [Weblate](https://weblate.fluxer.tools), not through pull requests.
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-discussion.json
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Search existing discussions before posting a feature proposal.
|
||||
|
||||
Report vulnerabilities through the [private form](https://github.com/fluxerapp/fluxer/security/advisories/new) or <[email protected]>.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Current problem
|
||||
description: State what you are trying to do and what prevents it.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed change
|
||||
description: State the expected behaviour.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Optional. Include constraints, trade-offs, related discussions, screenshots or mockups.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I searched existing discussions.
|
||||
required: true
|
||||
@@ -1,83 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json
|
||||
name: Bug report
|
||||
description: Report a reproducible defect in Fluxer.
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Search [open and closed issues](https://github.com/fluxerapp/fluxer/issues?q=is%3Aissue) before filing a report.
|
||||
|
||||
Report vulnerabilities through the [private form](https://github.com/fluxerapp/fluxer/security/advisories/new) or <[email protected]>. Send account and billing requests to <[email protected]>.
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Observed behaviour
|
||||
description: State what happened and what you expected.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Give numbered steps starting from a fresh app or session.
|
||||
placeholder: |
|
||||
1. Go to ...
|
||||
2. Select ...
|
||||
3. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: build
|
||||
attributes:
|
||||
label: Build information
|
||||
description: >-
|
||||
Open User Settings, scroll to the bottom of the left sidebar, and select
|
||||
the build information. Fluxer copies it to the clipboard.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: surface
|
||||
attributes:
|
||||
label: Affected surface
|
||||
multiple: true
|
||||
options:
|
||||
- Desktop app
|
||||
- Web app
|
||||
- Voice, video, or Go Live
|
||||
- Self-hosted instance
|
||||
- HTTP API or Gateway
|
||||
- Documentation site
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: instance
|
||||
attributes:
|
||||
label: Instance
|
||||
description: For a self-hosted instance, include the release tag and database backend.
|
||||
placeholder: fluxer.app
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Evidence
|
||||
description: Attach relevant logs, screenshots or recordings. Remove tokens, keys, private messages and other personal data. Configuration files may contain secrets.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I searched open and closed issues.
|
||||
required: true
|
||||
- label: I removed secrets and unrelated personal data from the report.
|
||||
required: true
|
||||
@@ -1,18 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Mobile client bugs
|
||||
url: https://github.com/fluxerapp/flutter_client#bug-reporting
|
||||
about: Read the reporting instructions for the Fluxer mobile client.
|
||||
- name: Account and billing support
|
||||
url: https://fluxer.app/help
|
||||
about: Find account help and support contact details.
|
||||
- name: Feature proposals
|
||||
url: https://github.com/orgs/fluxerapp/discussions
|
||||
about: Propose a feature in a discussion.
|
||||
- name: Translations
|
||||
url: https://weblate.fluxer.tools
|
||||
about: Improve an existing locale or start a new one.
|
||||
- name: Self-hosting support
|
||||
url: https://fluxer.dev
|
||||
about: Read the operator documentation, then open a discussion if the problem remains.
|
||||
@@ -1,44 +0,0 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json
|
||||
name: Documentation
|
||||
description: Report incorrect, missing or unclear documentation.
|
||||
type: Task
|
||||
labels:
|
||||
- docs
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This form covers <https://fluxer.dev> and operator documentation.
|
||||
|
||||
- type: textarea
|
||||
id: issue
|
||||
attributes:
|
||||
label: Documentation defect
|
||||
description: State what the page says and what is correct. For missing content, state what information you needed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: location
|
||||
attributes:
|
||||
label: Location
|
||||
description: Provide the page URL or file path and heading.
|
||||
placeholder: https://fluxer.dev/gateway/overview/
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: Proposed wording
|
||||
description: Optional.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
options:
|
||||
- label: I searched open and closed issues.
|
||||
required: true
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# Security policy
|
||||
|
||||
Do not report a vulnerability in an issue, pull request, or discussion.
|
||||
Do not report a vulnerability in a pull request or on feedback.fluxer.com.
|
||||
|
||||
Submit a report through [GitHub private vulnerability reporting](https://github.com/fluxerapp/fluxer/security/advisories/new) or email <security@fluxer.com>. Include the affected component, impact, reproduction steps, and supporting evidence. Remove unrelated personal data and secrets.
|
||||
Submit a report through <https://fluxer.app/security> or email <security@fluxer.com>. Include the affected component, impact, reproduction steps, and supporting evidence. Remove unrelated personal data and secrets.
|
||||
|
||||
The programme scope, testing rules, safe harbour, disclosure process, and reward terms are published at <https://fluxer.app/security>. That page is authoritative.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Closes #
|
||||
Resolves https://feedback.fluxer.com/p/
|
||||
|
||||
<!-- Repeat this line for each resolved issue, up to 20. Remove the placeholder only if no issue is resolved and the approval gate does not apply. -->
|
||||
<!-- Repeat this line for each feedback.fluxer.com post this resolves, up to 20. Remove the placeholder only if no post is resolved and the approval gate does not apply. -->
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
|
||||
# Fluxer
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Bug reports and feature requests have moved to [feedback.fluxer.com](https://feedback.fluxer.com). Sign in with your Fluxer account to post, vote and follow updates. GitHub Issues and Discussions are closed. Report security vulnerabilities privately through [fluxer.app/security](https://fluxer.app/security).
|
||||
|
||||
Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -69,6 +69,7 @@ import {shouldUseKeyboardShortcutsOverlayFallbackFromEvent} from '@app/features/
|
||||
import {jsKeyToUiohookKeycode} from '@app/features/input/utils/UiohookKeycodes';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import MessageFocus from '@app/features/messaging/state/MessageFocus';
|
||||
import {getSortedDmChannels} from '@app/features/messaging/utils/DmChannelUtils';
|
||||
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
|
||||
import Navigation from '@app/features/navigation/state/Navigation';
|
||||
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
|
||||
@@ -374,7 +375,7 @@ class KeybindManager {
|
||||
}
|
||||
|
||||
private cycleDirectMessageContext(direction: 1 | -1): void {
|
||||
const dmChannels = Channels.dmChannels;
|
||||
const dmChannels = getSortedDmChannels(Channels.dmChannels, Authentication.currentUserId);
|
||||
const slotCount = dmChannels.length + 1;
|
||||
const currentChannelId = this.currentChannelId;
|
||||
const currentIndex = currentChannelId ? dmChannels.findIndex((channel) => channel.id === currentChannelId) + 1 : 0;
|
||||
|
||||
@@ -926,6 +926,7 @@ export async function crosspost(i18n: I18n, channelId: string, messageId: string
|
||||
export function deleteLocal(channelId: string, messageId: string): void {
|
||||
logger.debug(`Deleting message ${messageId} locally in channel ${channelId}`);
|
||||
Messages.handleMessageDelete({id: messageId, channelId});
|
||||
MessageReply.handleMessageDelete(channelId, messageId);
|
||||
}
|
||||
|
||||
export function revealMessage(channelId: string, messageId: string | null): void {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import ChannelPins from '@app/features/channel/state/ChannelPins';
|
||||
import type {GatewayHandlerContext} from '@app/features/gateway/events/EventRouter';
|
||||
import MessageReferences from '@app/features/messaging/state/MessageReferences';
|
||||
import MessageReply from '@app/features/messaging/state/MessageReply';
|
||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||
import SavedMessages from '@app/features/messaging/state/SavedMessages';
|
||||
import MentionFeed from '@app/features/notification/state/MentionFeed';
|
||||
@@ -20,6 +21,7 @@ export function handleMessageDelete(data: MessageDeletePayload, _context: Gatewa
|
||||
ChannelPins.handleMessageDelete(data.channel_id, data.id);
|
||||
Messages.handleMessageDelete({channelId: data.channel_id, id: data.id});
|
||||
MessageReferences.handleMessageDelete(data.channel_id, data.id);
|
||||
MessageReply.handleMessageDelete(data.channel_id, data.id);
|
||||
ReadStates.handleMessageDelete({channelId: data.channel_id});
|
||||
MentionFeed.handleMessageDelete(data.id);
|
||||
Notification.handleMessageDelete({channelId: data.channel_id});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type {GatewayHandlerContext} from '@app/features/gateway/events/EventRouter';
|
||||
import MessageReferences from '@app/features/messaging/state/MessageReferences';
|
||||
import MessageReply from '@app/features/messaging/state/MessageReply';
|
||||
import Messages from '@app/features/messaging/state/MessagingMessages';
|
||||
import ReadStates from '@app/features/read_state/state/ReadStates';
|
||||
import Notification from '@app/features/ui/state/Notification';
|
||||
@@ -14,6 +15,7 @@ interface MessageDeleteBulkPayload {
|
||||
export function handleMessageDeleteBulk(data: MessageDeleteBulkPayload, _context: GatewayHandlerContext): void {
|
||||
Messages.handleMessageDeleteBulk({channelId: data.channel_id, ids: data.ids});
|
||||
MessageReferences.handleMessageDeleteBulk(data.channel_id, data.ids);
|
||||
MessageReply.handleMessageDeleteBulk(data.channel_id, data.ids);
|
||||
ReadStates.handleMessageDelete({channelId: data.channel_id});
|
||||
Notification.handleMessageDelete({channelId: data.channel_id});
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import MessageEdit from '@app/features/messaging/state/MessageEdit';
|
||||
import MessageFocus from '@app/features/messaging/state/MessageFocus';
|
||||
import {insertTextAtCursor} from '@app/features/messaging/utils/TextInputEditUtils';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import {canFocusTextarea, safeFocus} from '@app/features/platform/utils/InputFocusManager';
|
||||
import {isTextInputKeyEvent} from '@app/features/platform/utils/IsTextInputKeyEvent';
|
||||
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
||||
import ContextMenu from '@app/features/ui/state/ContextMenu';
|
||||
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
import {useCallback, useEffect} from 'react';
|
||||
|
||||
interface UseTextareaKeyboardOptions {
|
||||
channelId: string;
|
||||
isFocused: boolean;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
handleTextChange: (newValue: string, previousValue: string) => void;
|
||||
previousValueRef: React.RefObject<string>;
|
||||
clearSegments: () => void;
|
||||
replyingMessage: {
|
||||
messageId: string;
|
||||
mentioning: boolean;
|
||||
} | null;
|
||||
editingMessage: Message | null;
|
||||
getLastEditableMessage: () => Message | null;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
type ArrowUpEditShortcutEvent = Pick<
|
||||
React.KeyboardEvent | KeyboardEvent,
|
||||
'key' | 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'defaultPrevented'
|
||||
>;
|
||||
|
||||
export const shouldStartLastMessageEditFromArrowUp = (event: ArrowUpEditShortcutEvent, value: string): boolean => {
|
||||
if (event.defaultPrevented) return false;
|
||||
if (event.key !== 'ArrowUp') return false;
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return false;
|
||||
return value.length === 0;
|
||||
};
|
||||
export const useTextareaKeyboard = ({
|
||||
channelId,
|
||||
isFocused,
|
||||
textareaRef,
|
||||
value,
|
||||
setValue,
|
||||
handleTextChange,
|
||||
previousValueRef,
|
||||
clearSegments,
|
||||
replyingMessage,
|
||||
editingMessage,
|
||||
getLastEditableMessage,
|
||||
enabled,
|
||||
}: UseTextareaKeyboardOptions) => {
|
||||
const mobileLayout = MobileLayout;
|
||||
const editingMessageId = MessageEdit.getEditingMessageId(channelId);
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!canFocusTextarea(textarea || undefined)) {
|
||||
return;
|
||||
}
|
||||
if (isFocused) {
|
||||
return;
|
||||
}
|
||||
if (QuickSwitcher.getIsOpen()) {
|
||||
return;
|
||||
}
|
||||
if (ContextMenu.contextMenu) {
|
||||
return;
|
||||
}
|
||||
if (KeyboardMode.keyboardModeEnabled && MessageFocus.focusedMessageId) {
|
||||
return;
|
||||
}
|
||||
if (!isTextInputKeyEvent(event)) {
|
||||
return;
|
||||
}
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Dead') {
|
||||
safeFocus(textarea, true);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
safeFocus(textarea, true);
|
||||
const inserted = insertTextAtCursor(textarea, event.key);
|
||||
if (!inserted) {
|
||||
setValue((prev) => {
|
||||
const newValue = prev + event.key;
|
||||
handleTextChange(newValue, previousValueRef.current ?? '');
|
||||
return newValue;
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
editingMessageId,
|
||||
isFocused,
|
||||
mobileLayout.enabled,
|
||||
handleTextChange,
|
||||
previousValueRef,
|
||||
textareaRef,
|
||||
setValue,
|
||||
enabled,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
const isEditingInline = MessageEdit.getEditingMessageId(channelId) != null;
|
||||
if (isEditingInline) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
MessageCommands.stopEdit(channelId);
|
||||
return;
|
||||
}
|
||||
if (editingMessage && mobileLayout.enabled) {
|
||||
event.preventDefault();
|
||||
MessageCommands.stopEditMobile(channelId);
|
||||
setValue('');
|
||||
clearSegments();
|
||||
} else if (replyingMessage) {
|
||||
event.preventDefault();
|
||||
MessageCommands.stopReply(channelId);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
ComponentBus.dispatch('ESCAPE_PRESSED');
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [channelId, replyingMessage, editingMessage, mobileLayout.enabled, clearSegments, setValue, enabled]);
|
||||
const handleArrowUp = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (!shouldStartLastMessageEditFromArrowUp(event, value)) {
|
||||
return;
|
||||
}
|
||||
if (KeyboardMode.keyboardModeEnabled) {
|
||||
event.preventDefault();
|
||||
ComponentBus.dispatch('FOCUS_BOTTOMMOST_MESSAGE', {channelId});
|
||||
return;
|
||||
}
|
||||
const message = getLastEditableMessage();
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
MessageCommands.startEdit(channelId, message.id, message.content);
|
||||
},
|
||||
[channelId, value, getLastEditableMessage],
|
||||
);
|
||||
return {handleArrowUp};
|
||||
};
|
||||
@@ -56,6 +56,19 @@ class MessageReply {
|
||||
delete this.replyingMessageIds[channelId];
|
||||
}
|
||||
|
||||
handleMessageDelete(channelId: string, messageId: string): void {
|
||||
if (this.replyingMessageIds[channelId]?.messageId === messageId) {
|
||||
delete this.replyingMessageIds[channelId];
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageDeleteBulk(channelId: string, messageIds: Array<string>): void {
|
||||
const current = this.replyingMessageIds[channelId];
|
||||
if (current && messageIds.includes(current.messageId)) {
|
||||
delete this.replyingMessageIds[channelId];
|
||||
}
|
||||
}
|
||||
|
||||
highlightMessage(messageId: string): void {
|
||||
this.highlightMessageId = messageId;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,12 @@ class Messages {
|
||||
|
||||
getLastEditableMessage(channelId: string): Message | undefined {
|
||||
return this.getMessages(channelId).searchFromNewest((message) => {
|
||||
return message.isCurrentUserAuthor() && message.state === MessageStates.SENT && message.isUserMessage();
|
||||
return (
|
||||
message.isCurrentUserAuthor() &&
|
||||
message.state === MessageStates.SENT &&
|
||||
message.isUserMessage() &&
|
||||
!message.messageSnapshots
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<image type="source" width="2560" height="2227">https://fluxer.app/static/img/screenshots-desktop-2560w.948e9d88f4c9807a.avif</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="bugtracker">https://github.com/fluxerapp/fluxer/issues</url>
|
||||
<url type="bugtracker">https://feedback.fluxer.com</url>
|
||||
<url type="homepage">https://fluxer.app</url>
|
||||
<url type="donation">https://fluxer.app/donate</url>
|
||||
<url type="contact">https://fluxer.app/company-information#contact</url>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<image type="source" width="1920" height="1670">https://fluxer.app/static/img/screenshots-desktop-1920w.4976d2d1f2238c05.avif</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="bugtracker">https://github.com/fluxerapp/fluxer/issues</url>
|
||||
<url type="bugtracker">https://feedback.fluxer.com</url>
|
||||
<url type="homepage">https://canary.fluxer.app</url>
|
||||
<url type="donation">https://fluxer.app/donate</url>
|
||||
<url type="vcs-browser">https://github.com/fluxerapp/fluxer.git</url>
|
||||
|
||||
@@ -215,7 +215,7 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
|
||||
{
|
||||
label: t('desktop.appMenu.reportIssue'),
|
||||
click: async () => {
|
||||
await openExternalDeduped('https://github.com/fluxerapp/fluxer/issues');
|
||||
await openExternalDeduped('https://feedback.fluxer.com');
|
||||
},
|
||||
},
|
||||
{type: 'separator'},
|
||||
|
||||
@@ -2198,6 +2198,7 @@ async function verifyInstallerExecution(installerRoot: string): Promise<Array<st
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${stubBin}${path.delimiter}${process.env.PATH ?? ''}`,
|
||||
FLUXER_INSTALLER_REFRESHED: '1',
|
||||
...(cwd == null ? {} : {PWD: cwd}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -240,7 +240,7 @@ Once email is on, the address goes through a DNS check before the account exists
|
||||
|
||||
Sign in to the admin dashboard at `https://chat.example.com/admin` with the account holding the wildcard ACL. The **Instance Config** page has everything the wizard asked, plus registration mode, approvals and integration keys. **Limit Config** holds the instance limits published to clients. **Voice Regions** and **Voice Servers** come seeded, so voice needs no setup there.
|
||||
|
||||
The desktop client opens the hosted web app for its release channel, so reach your own instance in a browser. [Issue #1088](https://github.com/fluxerapp/fluxer/issues/1088) tracks that, and is being worked on.
|
||||
The desktop client opens the hosted web app for its release channel, so reach your own instance in a browser. [This feedback post](https://feedback.fluxer.com/p/641) tracks that, and is being worked on.
|
||||
|
||||
[Configuration](/operator/configuration/) lists every runtime setting and the environment variable each one overrides. [Deployment availability](/http-api/deployment-availability/) lists the routes that exist only on the hosted deployment.
|
||||
|
||||
|
||||
@@ -128,6 +128,8 @@ Removing the line from `.env` also lets the upgrade run, and it is the wrong fix
|
||||
|
||||
Keep the installer beside the stack files. [Get started](/operator/get-started/#step-4-bring-up-the-instance) has the download and the checksum check for a fresh copy.
|
||||
|
||||
`--update` first compares the installer against the digest at `https://fluxer.dev/install.sh.sha256`. When the copy differs, it downloads the current one, checks it against that digest, and asks before it replaces the copy and runs it with the same options. Answering no, `--non-interactive`, or a run without a terminal stops before anything changes. On Windows the same check reads `https://fluxer.dev/install.ps1.sha256`.
|
||||
|
||||
See the plan first:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -59,11 +59,17 @@ param(
|
||||
[string[]]$Rest = @()
|
||||
)
|
||||
|
||||
$FluxerScriptArguments = @{}
|
||||
foreach ($entry in $PSBoundParameters.GetEnumerator()) {
|
||||
$FluxerScriptArguments[$entry.Key] = $entry.Value
|
||||
}
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$FluxerRawBase = 'https://raw.githubusercontent.com/fluxerapp/fluxer'
|
||||
$FluxerInstallerUrl = 'https://fluxer.dev/install.ps1'
|
||||
$FluxerStackPath = 'deploy/self-hosting'
|
||||
$FluxerHealthPath = '/_health'
|
||||
$FluxerInitService = 'seaweedfs-init'
|
||||
@@ -1980,6 +1986,71 @@ function Assert-FluxerComposeFiles([string]$TargetDir, [string]$EnvPath) {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-FluxerSha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Update-FluxerInstaller {
|
||||
if ($env:FLUXER_INSTALLER_REFRESHED) {
|
||||
return
|
||||
}
|
||||
$self = $PSCommandPath
|
||||
if (-not $self) {
|
||||
return
|
||||
}
|
||||
$selfDir = Split-Path -Parent $self
|
||||
if ($DryRun) {
|
||||
$staging = New-FluxerStagingDirectory ([System.IO.Path]::GetTempPath())
|
||||
} else {
|
||||
$staging = New-FluxerStagingDirectory $selfDir
|
||||
}
|
||||
try {
|
||||
$digestPath = Join-Path $staging 'install.ps1.sha256'
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$FluxerInstallerUrl.sha256" -OutFile $digestPath -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 60
|
||||
} catch {
|
||||
Write-FluxerLine "Could not reach $FluxerInstallerUrl.sha256, so this run goes on with $self."
|
||||
return
|
||||
}
|
||||
$published = ([System.IO.File]::ReadAllText($digestPath).Trim() -split '\s+')[0].ToLowerInvariant()
|
||||
if ($published -notmatch '^[0-9a-f]{64}$') {
|
||||
Stop-Fluxer "$FluxerInstallerUrl.sha256 holds no sha256 digest. Nothing was changed." $FluxerExitDownload
|
||||
}
|
||||
if ((Get-FluxerSha256 $self) -eq $published) {
|
||||
return
|
||||
}
|
||||
$fresh = Join-Path $staging 'install.ps1'
|
||||
try {
|
||||
Invoke-WebRequest -Uri $FluxerInstallerUrl -OutFile $fresh -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 120
|
||||
} catch {
|
||||
Stop-Fluxer "Download failed for $FluxerInstallerUrl. Nothing was changed." $FluxerExitDownload
|
||||
}
|
||||
if ((Get-FluxerSha256 $fresh) -ne $published) {
|
||||
Stop-Fluxer "$FluxerInstallerUrl does not match the digest in $FluxerInstallerUrl.sha256. Nothing was changed." $FluxerExitDownload
|
||||
}
|
||||
Write-FluxerLine "$self differs from the installer $FluxerInstallerUrl serves. The stack files an upgrade downloads can require .env keys that only the current installer writes."
|
||||
if ($DryRun) {
|
||||
Write-FluxerLine 'The run asks to replace it with the current installer before it changes anything. The plan below is the one this copy would follow.'
|
||||
return
|
||||
}
|
||||
if ($NonInteractive -or [Console]::IsInputRedirected) {
|
||||
Stop-Fluxer "Nothing was changed. Download the current installer and run it:`n Invoke-WebRequest -Uri $FluxerInstallerUrl -OutFile install.ps1 -UseBasicParsing" $FluxerExitRefused
|
||||
}
|
||||
$answer = Read-Host -Prompt "Replace $self with the current installer and run that? [y/N]"
|
||||
if ($null -eq $answer -or $answer.Trim() -notmatch '^(y|yes)$') {
|
||||
Stop-Fluxer "Kept $self. Nothing was changed. Read the current installer at $FluxerInstallerUrl and run it once it is in place." $FluxerExitRefused
|
||||
}
|
||||
Move-Item -LiteralPath $fresh -Destination $self -Force
|
||||
} finally {
|
||||
Remove-FluxerStagingDirectory $staging
|
||||
}
|
||||
Write-FluxerLine "Replaced $self. Running it."
|
||||
$env:FLUXER_INSTALLER_REFRESHED = '1'
|
||||
$global:LASTEXITCODE = 0
|
||||
& $self @FluxerScriptArguments
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
function Invoke-FluxerInstall {
|
||||
if ($Help) {
|
||||
Show-FluxerUsage
|
||||
@@ -2022,6 +2093,10 @@ function Invoke-FluxerInstall {
|
||||
|
||||
Invoke-FluxerPreflight
|
||||
|
||||
if ($Update) {
|
||||
Update-FluxerInstaller
|
||||
}
|
||||
|
||||
$targetPath = $Dir
|
||||
$adoptedCwd = $false
|
||||
$fellBack = $false
|
||||
|
||||
@@ -51,6 +51,7 @@ LC_ALL=C
|
||||
export LC_ALL
|
||||
|
||||
FLUXER_RAW_BASE='https://raw.githubusercontent.com/fluxerapp/fluxer'
|
||||
FLUXER_INSTALLER_URL='https://fluxer.dev/install.sh'
|
||||
FLUXER_STACK_PATH='deploy/self-hosting'
|
||||
FLUXER_MIN_ENGINE='24.0.0'
|
||||
# Podman numbers its releases on its own scale, so the Docker Engine floor says
|
||||
@@ -305,6 +306,18 @@ opt_no_volume_compression=0
|
||||
opt_skip_backup=0
|
||||
opt_allow_root=0
|
||||
|
||||
fluxer_self=''
|
||||
if [ -f "$0" ]; then
|
||||
case $0 in
|
||||
/*) fluxer_self=$0 ;;
|
||||
*) fluxer_self="$(pwd)/$0" ;;
|
||||
esac
|
||||
fi
|
||||
fluxer_self_args=''
|
||||
for fluxer_arg in "$@"; do
|
||||
fluxer_self_args="$fluxer_self_args '$(printf '%s' "$fluxer_arg" | sed "s/'/'\\\\''/g")'"
|
||||
done
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--domain)
|
||||
@@ -651,6 +664,74 @@ fluxer_prompt() {
|
||||
return 1
|
||||
}
|
||||
|
||||
fluxer_sha256() {
|
||||
openssl dgst -sha256 < "$1" | awk '{print $NF}'
|
||||
}
|
||||
|
||||
fluxer_drop_scratch() {
|
||||
fluxer_cleanup
|
||||
fluxer_scratch=''
|
||||
}
|
||||
|
||||
fluxer_refresh_installer() {
|
||||
[ -z "${FLUXER_INSTALLER_REFRESHED:-}" ] || return 0
|
||||
[ -n "$fluxer_self" ] || return 0
|
||||
fluxer_self_dir=$(dirname "$fluxer_self")
|
||||
if [ "$opt_dry_run" -eq 1 ]; then
|
||||
fluxer_open_scratch "${TMPDIR:-/tmp}"
|
||||
elif [ -w "$fluxer_self_dir" ]; then
|
||||
fluxer_open_scratch "$fluxer_self_dir"
|
||||
else
|
||||
fluxer_say "$fluxer_self_dir is not writable, so this run cannot check $fluxer_self against $FLUXER_INSTALLER_URL and goes on with it."
|
||||
return 0
|
||||
fi
|
||||
if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$fluxer_scratch/install.sh.sha256" "$FLUXER_INSTALLER_URL.sha256"; then
|
||||
fluxer_say "Could not reach $FLUXER_INSTALLER_URL.sha256, so this run goes on with $fluxer_self."
|
||||
fluxer_drop_scratch
|
||||
return 0
|
||||
fi
|
||||
fluxer_published=$(awk 'NR == 1 {print $1}' "$fluxer_scratch/install.sh.sha256")
|
||||
case $fluxer_published in
|
||||
''|*[!0-9a-f]*) fluxer_fail 4 "$FLUXER_INSTALLER_URL.sha256 holds no sha256 digest. Nothing was changed." ;;
|
||||
esac
|
||||
if [ "$(fluxer_sha256 "$fluxer_self")" = "$fluxer_published" ]; then
|
||||
fluxer_drop_scratch
|
||||
return 0
|
||||
fi
|
||||
if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$fluxer_scratch/install.sh" "$FLUXER_INSTALLER_URL"; then
|
||||
fluxer_fail 4 "Download failed for $FLUXER_INSTALLER_URL. Nothing was changed."
|
||||
fi
|
||||
if [ "$(fluxer_sha256 "$fluxer_scratch/install.sh")" != "$fluxer_published" ]; then
|
||||
fluxer_fail 4 "$FLUXER_INSTALLER_URL does not match the digest in $FLUXER_INSTALLER_URL.sha256. Nothing was changed."
|
||||
fi
|
||||
fluxer_say "$fluxer_self differs from the installer $FLUXER_INSTALLER_URL serves. The stack files an upgrade downloads can require .env keys that only the current installer writes."
|
||||
if [ "$opt_dry_run" -eq 1 ]; then
|
||||
fluxer_say 'The run asks to replace it with the current installer before it changes anything. The plan below is the one this copy would follow.'
|
||||
fluxer_drop_scratch
|
||||
return 0
|
||||
fi
|
||||
if [ "$opt_non_interactive" -eq 1 ] || [ ! -t 0 ]; then
|
||||
fluxer_fail 3 "Nothing was changed. Download the current installer and run it:
|
||||
curl -fsSLO $FLUXER_INSTALLER_URL"
|
||||
fi
|
||||
printf 'Replace %s with the current installer and run that? [y/N] ' "$fluxer_self" >&2
|
||||
fluxer_answer=''
|
||||
read -r fluxer_answer || true
|
||||
case $fluxer_answer in
|
||||
y|Y|yes|Yes|YES) ;;
|
||||
*) fluxer_fail 3 "Kept $fluxer_self. Nothing was changed. Read the current installer at $FLUXER_INSTALLER_URL and run it once it is in place." ;;
|
||||
esac
|
||||
if [ -x "$fluxer_self" ]; then
|
||||
chmod +x "$fluxer_scratch/install.sh"
|
||||
fi
|
||||
mv "$fluxer_scratch/install.sh" "$fluxer_self"
|
||||
fluxer_drop_scratch
|
||||
fluxer_say "Replaced $fluxer_self. Running it."
|
||||
FLUXER_INSTALLER_REFRESHED=1
|
||||
export FLUXER_INSTALLER_REFRESHED
|
||||
eval "exec sh \"\$fluxer_self\" $fluxer_self_args"
|
||||
}
|
||||
|
||||
fluxer_resolve_values() {
|
||||
if [ "$opt_update" -eq 1 ] || [ "$opt_rollback" -eq 1 ]; then
|
||||
return 0
|
||||
@@ -2100,6 +2181,10 @@ fluxer_preflight
|
||||
fluxer_validate_options
|
||||
fluxer_resolve_values
|
||||
|
||||
if [ "$opt_update" -eq 1 ]; then
|
||||
fluxer_refresh_installer
|
||||
fi
|
||||
|
||||
if [ "$opt_update" -eq 1 ] || [ "$opt_rollback" -eq 1 ]; then
|
||||
fluxer_require_instance
|
||||
fluxer_resolve_ref
|
||||
|
||||
Reference in New Issue
Block a user