Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9496b0f
chore: update oxlint to 1.80
diegolmello Aug 26, 2026
f9a7d06
chore: install anti-slop oxlint plugin
diegolmello Aug 26, 2026
d7e8a25
refactor: satisfy anti-slop object-parameter, empty-spread and shape-…
diegolmello Aug 26, 2026
c7c5bb8
refactor: tighten unknown boundaries in Deferred, jump anchor and pre…
diegolmello Aug 26, 2026
46b77bb
chore: disable anti-slop unknown-boundary rules for test files
diegolmello Aug 26, 2026
9a3011b
refactor: satisfy anti-slop no-unknown-parameters
diegolmello Aug 26, 2026
92b6f1a
chore: remove anti-slop no-unknown-parameters rule
diegolmello Aug 26, 2026
ad283fa
chore: remove anti-slop no-unknown-returns rule
diegolmello Aug 26, 2026
a9b968c
refactor: satisfy anti-slop no-known-value-widening
diegolmello Aug 26, 2026
fb8f1da
chore: exempt runtime-keyed dictionaries from anti-slop no-known-valu…
diegolmello Aug 26, 2026
db3c7de
chore: remove anti-slop no-runtime-typeof rule
diegolmello Aug 26, 2026
ea74a0c
chore: demote anti-slop no-chained-type-assertions to warn
diegolmello Aug 26, 2026
20f8147
chore: demote anti-slop no-unsafe-dictionary-type to warn
diegolmello Aug 26, 2026
4a7f55b
fix: use a type import for the helpers module mock
diegolmello Aug 26, 2026
39110f0
docs: record anti-slop plugin upstream and local deviations
diegolmello Aug 26, 2026
1e7b0aa
fix: keep info-card action props omitted when absent
diegolmello Aug 26, 2026
e60ef4f
chore: drop unvendored anti-slop effect rules
diegolmello Aug 26, 2026
5b92e01
fix: reject inherited keys in record type guards
diegolmello Aug 26, 2026
a674b0e
fix: restore explicit return types on exported APIs
diegolmello Aug 26, 2026
199239f
chore: keep omitted message type out of test fixtures
diegolmello Aug 26, 2026
d9e4e9a
refactor: build the OAuth state object once
diegolmello Aug 26, 2026
e4c87a3
refactor: extract hasOwnKey and keep the exhaustive status table
diegolmello Aug 26, 2026
73fb6b2
refactor: name the OAuth state payload at its only use
diegolmello Aug 26, 2026
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
3 changes: 2 additions & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
".storybook/",
"app/i18n/locales/",
"app/containers/CustomIcon/mappedIcons.js",
"app/containers/CustomIcon/selection.json"
"app/containers/CustomIcon/selection.json",
"tools/oxlint/anti-slop/"
]
}
36 changes: 33 additions & 3 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "react", "jest"],
"jsPlugins": ["eslint-plugin-react-native"],
"jsPlugins": [
"eslint-plugin-react-native",
{
"name": "anti-slop",
"specifier": "./tools/oxlint/anti-slop/index.ts"
}
],
"categories": {
"correctness": "off"
},
Expand All @@ -17,7 +23,20 @@
"**/android",
"**/ios",
"**/.worktrees/",
".rnstorybook/storybook.requires.ts"
".rnstorybook/storybook.requires.ts",
".agent/**",
".agents/**",
".claude/**",
".codex/**",
".continue/**",
".cursor/**",
".gemini/**",
".opencode/**",
".pi/**",
".roo/**",
".windsurf/**",
".sniffler/**",
"tools/oxlint/anti-slop/**"
],
"rules": {
"import/extensions": "off",
Expand All @@ -42,7 +61,18 @@
"require-await": "error",
"react/exhaustive-deps": "warn",
"react/rules-of-hooks": "error",
"react/react-compiler": "warn"
"anti-slop/no-chained-type-assertions": "warn",
"anti-slop/no-conditional-empty-object-spread": "error",
"anti-slop/no-known-value-widening": "error",
"anti-slop/no-module-mocking": "warn",
"anti-slop/no-object-parameters": "error",
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-shape-in-symbol-names": "error",
"anti-slop/no-unknown-type-aliases": "error",
"anti-slop/no-unsafe-dictionary-type": "warn",
"anti-slop/no-widen-then-assert": "error",
"anti-slop/require-safety-comment-for-type-assertion": "warn"
},
"overrides": [
{
Expand Down
1 change: 1 addition & 0 deletions app/actions/actionsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base = {}, types = defaultTypes): Record<string, string> {
const res: Record<string, string> = {};
types.forEach(type => (res[type] = `${base}_${type}`));
// oxlint-disable-next-line anti-slop/no-known-value-widening -- the action-type keys come from a runtime array, not a literal
return res;
}

Expand Down
4 changes: 1 addition & 3 deletions app/containers/List/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ const styles = StyleSheet.create({
fontSize: 14,
...sharedStyles.textRegular
},
actionIndicator: {
...(I18nManager.isRTL ? { transform: [{ rotate: '180deg' }] } : {})
}
actionIndicator: I18nManager.isRTL ? { transform: [{ rotate: '180deg' }] } : {}
});

interface IListTitle extends Pick<IListItemContent, 'title' | 'color' | 'translateTitle' | 'styleTitle' | 'numberOfLines'> {}
Expand Down
15 changes: 2 additions & 13 deletions app/containers/LoginServices/serviceLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,8 @@ const openOAuthSession = async (url: string) => {

const getOAuthState = (loginStyle: TLoginStyle = 'popup') => {
const credentialToken = random(43);
let obj: {
loginStyle: string;
credentialToken: string;
isCordova: boolean;
redirectUrl?: string;
} = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === 'redirect') {
obj = {
...obj,
redirectUrl: 'rocketchat://auth'
};
}
return Base64.encodeURI(JSON.stringify(obj));
const state = { loginStyle, credentialToken, isCordova: true };
return Base64.encodeURI(JSON.stringify(loginStyle === 'redirect' ? { ...state, redirectUrl: 'rocketchat://auth' } : state));
};

const openSSOWebView = ({ url, ssoToken, authType }: IOpenSSOWebView) => {
Expand Down
4 changes: 2 additions & 2 deletions app/containers/MessageComposer/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export const MAX_HEIGHT = 200;

export const NO_CANNED_RESPONSES = 'no-canned-responses';

export const MARKDOWN_STYLES: Record<TMarkdownStyle, string> = {
export const MARKDOWN_STYLES = {
bold: '*',
italic: '_',
strike: '~',
code: '`',
'code-block': '```'
};
} satisfies Record<TMarkdownStyle, string>;

export const COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH = 30;
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ assertType<InsideStackParamList extends { CallView: unknown } ? true : false>();
// Jest hoists jest.mock() to the top of the module; factories can only close
// over variables whose names start with "mock". Wrapping in an object lets us
// reassign across the beforeEach without recreating the module mock.
const mockSdkState: { streamHandler: ((msg: IDDPMessage) => void) | null } = { streamHandler: null };
interface IMockSdkState {
streamHandler: ((msg: IDDPMessage) => void) | null;
}

const mockSdkState: IMockSdkState = { streamHandler: null };

jest.mock('../../lib/database', () => ({
db: { get: jest.fn() },
Expand Down
6 changes: 3 additions & 3 deletions app/containers/ThemeContextProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { useContext, useState } from 'react';
import { TouchableOpacity } from 'react-native';

import ThemeContextProvider from './ThemeContextProvider';
import { ThemeContext } from '../theme';
import { ThemeContext, type IThemeContextProps } from '../theme';
import type { IThemePreference } from '../definitions/ITheme';

const defaultPrefs: IThemePreference = { currentTheme: 'light', darkLevel: 'dark' };
const setTheme = jest.fn();

function ContextCapture({ onCapture }: { onCapture: (v: object) => void }) {
function ContextCapture({ onCapture }: { onCapture: (v: IThemeContextProps) => void }) {
const value = useContext(ThemeContext);
onCapture(value);
return null;
Expand All @@ -18,7 +18,7 @@ function ContextCapture({ onCapture }: { onCapture: (v: object) => void }) {
// Parent holds its own counter state; ThemeContextProvider receives fixed props.
// This forces ThemeContextProvider to re-render on parent state changes, exercising the
// useMemo dep-check rather than React's same-props bailout shortcut.
function ParentWithCounter({ onCapture }: { onCapture: (v: object) => void }) {
function ParentWithCounter({ onCapture }: { onCapture: (v: IThemeContextProps) => void }) {
const [count, setCount] = useState(0);
return (
<>
Expand Down
2 changes: 1 addition & 1 deletion app/containers/TwoFactor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const methods: IMethods = {
};

const TwoFactor = memo(() => {
const schema = yup.object().shape({
const schema = yup.object({
code: yup.string().required(I18n.t('Code_required'))
});
const { colors } = useTheme();
Expand Down
17 changes: 12 additions & 5 deletions app/containers/UIKit/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { StyleSheet, View } from 'react-native';
import { hasIcon, CustomIcon } from '../CustomIcon';
import { useTheme } from '../../theme';
import { type IIcon } from './interfaces';
import { hasOwnKey } from '../../lib/methods/helpers/hasOwnKey';

const iconAliases: Record<string, string> = {
const iconAliases = {
'phone-end': 'phone-off',
microphone: 'mic',
'microphone-disabled': 'mic-off',
audio: 'volume',
'audio-disabled': 'volume-off'
};
} satisfies Record<string, string>;

type TIconAlias = keyof typeof iconAliases;

const isIconAlias = (icon: string): icon is TIconAlias => hasOwnKey(iconAliases, icon);

const styles = StyleSheet.create({
frame: {
Expand All @@ -27,9 +32,11 @@ export const resolveIconName = (icon: string) => {
return icon as any;
}

const aliasedIcon = iconAliases[icon];
if (aliasedIcon && hasIcon(aliasedIcon)) {
return aliasedIcon as any;
if (isIconAlias(icon)) {
const aliasedIcon = iconAliases[icon];
if (hasIcon(aliasedIcon)) {
return aliasedIcon as any;
}
}

return 'info' as any;
Expand Down
4 changes: 2 additions & 2 deletions app/containers/UIKit/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ export const Select = ({ options = [], placeholder, onChange, loading, disabled,
const { theme } = useTheme();
const [selected, setSelected] = useState(!Array.isArray(initialValue) && initialValue);
const items = options.map(option => ({ label: textParser([option.text]), value: option.value }));
const pickerStyle = {
const basePickerStyle = {
...styles.viewContainer,
...(isIOS ? styles.iosPadding : {}),
borderColor: themes[theme].strokeLight,
backgroundColor: themes[theme].surfaceRoom
};
const pickerStyle = isIOS ? { ...basePickerStyle, ...styles.iosPadding } : basePickerStyle;

const placeholderObject = useMemo(
() =>
Expand Down
4 changes: 2 additions & 2 deletions app/containers/UIKit/UiKitMessage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,8 @@ const getInfoCardAction = ({
}) => ({
type: 'icon_button',
actionId: 'open-history',
...(appId ? { appId } : {}),
...(blockId ? { blockId } : {}),
...(appId && { appId }),
...(blockId && { blockId }),
label: label ?? 'Call history',
icon: {
type: 'icon',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const VideoConferenceBaseContainer = ({ variant, children }: VideoConfMes
const { colors } = useTheme();
const style = useStyle();

const iconStyle: { [key: string]: { icon: TIconsName; color: string; backgroundColor: string; label: string } } = {
const iconStyle = {
ended: {
icon: 'phone-off',
color: colors.fontSecondaryInfo,
Expand All @@ -40,7 +40,10 @@ export const VideoConferenceBaseContainer = ({ variant, children }: VideoConfMes
backgroundColor: colors.statusBackgroundWarning,
label: i18n.t('Call_issue')
}
};
} satisfies Record<
VideoConfMessageIconProps['variant'],
{ icon: TIconsName; color: string; backgroundColor: string; label: string }
>;

return (
<View style={style.container}>
Expand Down
5 changes: 3 additions & 2 deletions app/containers/message/hooks/useMessageAccessibilityLabel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import i18n from '../../../i18n';
import translationLanguages from '../../../lib/constants/translationLanguages';
import translationLanguages, { isTranslationLanguage } from '../../../lib/constants/translationLanguages';
import { useImageDescriptionLabel } from './useImageDescriptionLabel';
import { getInfoMessage } from '../utils';
import { type IUserChannel, type IUserMention } from '../../../definitions';
Expand Down Expand Up @@ -67,7 +67,8 @@ export const useMessageAccessibilityLabel = (): string => {
const readOrUnreadLabel = !unread && unread !== null ? i18n.t('Message_was_read') : i18n.t('Message_was_not_read');
const readReceipt = isReadReceiptEnabled && !isInfo ? readOrUnreadLabel : '';
const encryptedMessageLabel = isEncrypted ? i18n.t('Encrypted_message') : '';
const translatedLanguage = translationLanguages[autoTranslateLanguage || 'en'];
const language = autoTranslateLanguage || 'en';
const translatedLanguage = isTranslationLanguage(language) ? translationLanguages[language] : language;
const translated = isTranslated ? i18n.t('Message_translated_into_idiom', { idiom: translatedLanguage }) : '';
// For translated messages, the translated body is announced by the inner A11y.Index node, so the outer label
// only carries the metadata (user, hour, translated marker) and the suffix (image description, encryption, read receipt).
Expand Down
2 changes: 1 addition & 1 deletion app/containers/message/stores/MessageRoomStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type MessageRoomState = {
fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>;
toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>;
jumpToMessage?: (link: string) => void;
closeEmojiAndAction?: (action?: (params?: unknown) => void, params?: unknown) => void;
closeEmojiAndAction?: (action?: () => void) => void;
// row action handlers
onReactionPress?: (emoji: string, id: string) => void;
onReactionLongPress?: (item: TAnyMessageModel) => void;
Expand Down
8 changes: 7 additions & 1 deletion app/containers/message/stores/MessageStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,16 @@ export const useMessageIgnored = (): boolean => useMessageStore(s => (s.manualUn

export const useRevealIgnored = (): (() => void) => useMessageStore(s => s.reveal);

interface IUseMessageTouchableResult {
tappable: boolean;
longPressable: boolean;
revealsIgnored: boolean;
}

// Single source of truth for pressability, shared by the Touch gate, long-press guard and
// press guard. longPressable drops encrypted messages (tap can still open a thread; the action
// sheet is suppressed); revealsIgnored is tappable ∧ isIgnored (a tap reveals instead of pressing).
export const useMessageTouchable = (): { tappable: boolean; longPressable: boolean; revealsIgnored: boolean } => {
export const useMessageTouchable = (): IUseMessageTouchableResult => {
const isInfo = useIsInfoMessage();
const { hasError, isTemp } = useMessageStatus();
const isEncrypted = useIsEncrypted();
Expand Down
8 changes: 5 additions & 3 deletions app/definitions/TUserStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export const STATUSES = ['offline', 'online', 'away', 'busy', 'disabled', 'loadi

export type TUserStatus = (typeof STATUSES)[number];

export const STATUS_I18N_KEYS: Partial<Record<TUserStatus, string>> = {
export const STATUS_I18N_KEYS = {
online: 'Online',
away: 'Away',
busy: 'Busy',
offline: 'Offline'
};
offline: 'Offline',
disabled: undefined,
loading: undefined
} satisfies Record<TUserStatus, string | undefined>;
4 changes: 1 addition & 3 deletions app/ee/omnichannel/containers/OmnichannelHeader/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,5 @@ export default StyleSheet.create({
...sharedStyles.textRegular,
fontSize: 12
},
actionIndicator: {
...(I18nManager.isRTL ? { transform: [{ rotate: '180deg' }] } : {})
}
actionIndicator: I18nManager.isRTL ? { transform: [{ rotate: '180deg' }] } : {}
});
12 changes: 9 additions & 3 deletions app/i18n/dayjs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const localeKeys: { [key: string]: string } = {
import { hasOwnKey } from '../lib/methods/helpers/hasOwnKey';

const localeKeys = {
en: 'en',
ar: 'ar',
de: 'de',
Expand All @@ -17,6 +19,10 @@ const localeKeys: { [key: string]: string } = {
'zh-CN': 'zh-cn',
'zh-TW': 'zh-tw',
no: 'nb'
};
} satisfies Record<string, string>;

type TLocaleKey = keyof typeof localeKeys;

const isLocaleKey = (locale: string): locale is TLocaleKey => hasOwnKey(localeKeys, locale);

export const toDayJsLocale = (locale: string): string => localeKeys[locale] || locale;
export const toDayJsLocale = (locale: string): string => (isLocaleKey(locale) ? localeKeys[locale] : locale);
9 changes: 7 additions & 2 deletions app/lib/constants/keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { hasOwnKey } from '../methods/helpers/hasOwnKey';

export const E2E_MESSAGE_TYPE = 'e2e';
export const E2E_PUBLIC_KEY = 'RC_E2E_PUBLIC_KEY';
export const E2E_PRIVATE_KEY = 'RC_E2E_PRIVATE_KEY';
Expand All @@ -10,10 +12,13 @@ export const E2E_BANNER_TYPE = {
REQUEST_PASSWORD: 'REQUEST_PASSWORD',
SAVE_PASSWORD: 'SAVE_PASSWORD'
};
export const E2E_ROOM_TYPES: Record<string, string> = {
export const E2E_ROOM_TYPES = {
d: 'd',
p: 'p'
};
} satisfies Record<string, string>;

export const isE2ERoomType = (roomType?: string): roomType is keyof typeof E2E_ROOM_TYPES =>
roomType !== undefined && hasOwnKey(E2E_ROOM_TYPES, roomType);

export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY';
export const USER_MENTIONS_PREFERENCES_KEY = 'RC_USER_MENTIONS_PREFERENCES_KEY';
Expand Down
9 changes: 7 additions & 2 deletions app/lib/constants/translationLanguages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const translationLanguages: Record<string, string> = {
import { hasOwnKey } from '../methods/helpers/hasOwnKey';

const translationLanguages = {
af: 'Afrikaans',
ar: 'Arabic',
az: 'Azerbaijani',
Expand Down Expand Up @@ -200,6 +202,9 @@ const translationLanguages: Record<string, string> = {
yi: 'Yiddish',
yo: 'Yoruba',
yua: 'Yucatec Maya'
};
} satisfies Record<string, string>;

export const isTranslationLanguage = (language: string): language is keyof typeof translationLanguages =>
hasOwnKey(translationLanguages, language);

export default translationLanguages;
2 changes: 1 addition & 1 deletion app/lib/database/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ export const getSubscriptionSearchClause = (searchText: string): Q.Or => {
);
};

export const sanitizer = (r: object): object => r;
export const sanitizer = <T>(r: T): T => r;
Loading
Loading