diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 9af5ebcbfb..e2ae875ac3 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
 import SdkConfig from "./SdkConfig";
 import SettingsStore from "./settings/SettingsStore";
 import {UIFeature} from "./settings/UIFeature";
+import {CHAT_EFFECTS} from "./effects"
 import CallHandler from "./CallHandler";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
@@ -78,6 +79,7 @@ export const CommandCategories = {
     "actions": _td("Actions"),
     "admin": _td("Admin"),
     "advanced": _td("Advanced"),
+    "effects": _td("Effects"),
     "other": _td("Other"),
 };
 
@@ -1094,6 +1096,30 @@ export const Commands = [
         category: CommandCategories.messages,
         hideCompletionAfterSpace: true,
     }),
+
+    ...CHAT_EFFECTS.map((effect) => {
+        return new Command({
+            command: effect.command,
+            description: effect.description(),
+            args: '<message>',
+            runFn: function(roomId, args) {
+                return success((async () => {
+                    if (!args) {
+                        args = effect.fallbackMessage();
+                        MatrixClientPeg.get().sendEmoteMessage(roomId, args);
+                    } else {
+                        const content = {
+                            msgtype: effect.msgType,
+                            body: args,
+                        };
+                        MatrixClientPeg.get().sendMessage(roomId, content);
+                    }
+                    dis.dispatch({action: `effects.${effect.command}`});
+                })());
+            },
+            category: CommandCategories.effects,
+        })
+    }),
 ];
 
 // build a map from names and aliases to the Command objects.
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index adcb401ec1..d2d473fd3d 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -69,6 +69,9 @@ import AuxPanel from "../views/rooms/AuxPanel";
 import RoomHeader from "../views/rooms/RoomHeader";
 import {XOR} from "../../@types/common";
 import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
+import EffectsOverlay from "../views/elements/EffectsOverlay";
+import {containsEmoji} from '../../effects/utils';
+import {CHAT_EFFECTS} from '../../effects';
 import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
 import WidgetStore from "../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../stores/AsyncStore";
@@ -248,6 +251,8 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
         this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
         this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+        this.context.on("Event.decrypted", this.onEventDecrypted);
+        this.context.on("event", this.onEvent);
         // Start listening for RoomViewStore updates
         this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
         this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
@@ -581,6 +586,8 @@ export default class RoomView extends React.Component<IProps, IState> {
             this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
             this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
             this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
+            this.context.removeListener("Event.decrypted", this.onEventDecrypted);
+            this.context.removeListener("event", this.onEvent);
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
@@ -781,6 +788,27 @@ export default class RoomView extends React.Component<IProps, IState> {
         }
     };
 
+    private onEventDecrypted = (ev) => {
+        if (ev.isDecryptionFailure()) return;
+        this.handleEffects(ev);
+    };
+
+    private onEvent = (ev) => {
+        if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
+        this.handleEffects(ev);
+    };
+
+    private handleEffects = (ev) => {
+        if (!this.state.room ||
+            !this.state.matrixClientIsReady ||
+            this.state.room.getUnreadNotificationCount() === 0) return;
+        CHAT_EFFECTS.forEach(effect => {
+            if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
+                dis.dispatch({action: `effects.${effect.command}`});
+            }
+        })
+    };
+
     private onRoomName = (room: Room) => {
         if (this.state.room && room.roomId == this.state.room.roomId) {
             this.forceUpdate();
@@ -1946,9 +1974,14 @@ export default class RoomView extends React.Component<IProps, IState> {
             mx_RoomView_inCall: Boolean(activeCall),
         });
 
+        const showChatEffects = SettingsStore.getValue('showChatEffects');
+
         return (
             <RoomContext.Provider value={this.state}>
                 <main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
+                    {showChatEffects && this.roomView.current &&
+                        <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
+                    }
                     <ErrorBoundary>
                         <RoomHeader
                             room={this.state.room}
diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx
new file mode 100644
index 0000000000..6297d80768
--- /dev/null
+++ b/src/components/views/elements/EffectsOverlay.tsx
@@ -0,0 +1,94 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+import React, { FunctionComponent, useEffect, useRef } from 'react';
+import dis from '../../../dispatcher/dispatcher';
+import ICanvasEffect  from '../../../effects/ICanvasEffect';
+import {CHAT_EFFECTS} from '../../../effects'
+
+interface IProps {
+    roomWidth: number;
+}
+
+const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
+    const canvasRef = useRef<HTMLCanvasElement>(null);
+    const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>());
+
+    const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => {
+        if (!name) return null;
+        let effect: ICanvasEffect | null = effectsRef.current[name] || null;
+        if (effect === null) {
+            const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
+            try {
+                const { default: Effect } = await import(`../../../effects/${name}`);
+                effect = new Effect(options);
+                effectsRef.current[name] = effect;
+            } catch (err) {
+                console.warn('Unable to load effect module at \'../../../effects/${name}\'.', err);
+            }
+        }
+        return effect;
+    };
+
+    useEffect(() => {
+        const resize = () => {
+            if (canvasRef.current) {
+                canvasRef.current.height = window.innerHeight;
+            }
+        };
+        const onAction = (payload: { action: string }) => {
+            const actionPrefix = 'effects.';
+            if (payload.action.indexOf(actionPrefix) === 0) {
+                const effect = payload.action.substr(actionPrefix.length);
+                lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
+            }
+        }
+        const dispatcherRef = dis.register(onAction);
+        const canvas = canvasRef.current;
+        canvas.height = window.innerHeight;
+        window.addEventListener('resize', resize, true);
+
+        return () => {
+            dis.unregister(dispatcherRef);
+            window.removeEventListener('resize', resize);
+            // eslint-disable-next-line react-hooks/exhaustive-deps
+            const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
+            for (const effect in currentEffects) {
+                const effectModule: ICanvasEffect = currentEffects[effect];
+                if (effectModule && effectModule.isRunning) {
+                    effectModule.stop();
+                }
+            }
+        };
+    }, []);
+
+    return (
+        <canvas
+            ref={canvasRef}
+            width={roomWidth}
+            style={{
+                display: 'block',
+                zIndex: 999999,
+                pointerEvents: 'none',
+                position: 'fixed',
+                top: 0,
+                right: 0,
+            }}
+        />
+    )
+}
+
+export default EffectsOverlay;
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index e88060304a..8171da7eca 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -42,6 +42,8 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import {Action} from "../../../dispatcher/actions";
+import {containsEmoji} from "../../../effects/utils";
+import {CHAT_EFFECTS} from '../../../effects';
 import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 
@@ -326,6 +328,11 @@ export default class SendMessageComposer extends React.Component {
                 });
             }
             dis.dispatch({action: "message_sent"});
+            CHAT_EFFECTS.forEach((effect) => {
+                if (containsEmoji(content, effect.emojis)) {
+                    dis.dispatch({action: `effects.${effect.command}`});
+                }
+            });
             CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
         }
 
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index d6a4921f1a..4d8493401e 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -50,6 +50,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'showAvatarChanges',
         'showDisplaynameChanges',
         'showImages',
+        'showChatEffects',
         'Pill.shouldShowPillAvatar',
     ];
 
diff --git a/src/effects/ICanvasEffect.ts b/src/effects/ICanvasEffect.ts
new file mode 100644
index 0000000000..9bf3e9293d
--- /dev/null
+++ b/src/effects/ICanvasEffect.ts
@@ -0,0 +1,47 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+/**
+ * Defines the constructor of a canvas based room effect
+ */
+export interface ICanvasEffectConstructable {
+    /**
+     * @param {{[key:string]:any}} options? Optional animation options
+     * @returns ICanvasEffect Returns a new instance of the canvas effect
+     */
+    new(options?: { [key: string]: any }): ICanvasEffect;
+}
+
+/**
+ * Defines the interface of a canvas based room effect
+ */
+export default interface ICanvasEffect {
+    /**
+     * @param {HTMLCanvasElement} canvas The canvas instance as the render target of the animation
+     * @param {number} timeout? A timeout that defines the runtime of the animation (defaults to false)
+     */
+    start: (canvas: HTMLCanvasElement, timeout?: number) => Promise<void>;
+
+    /**
+     * Stops the current animation
+     */
+    stop: () => Promise<void>;
+
+    /**
+     * Returns a value that defines if the animation is currently running
+     */
+    isRunning: boolean;
+}
diff --git a/src/effects/confetti/index.ts b/src/effects/confetti/index.ts
new file mode 100644
index 0000000000..8c4b2d2616
--- /dev/null
+++ b/src/effects/confetti/index.ts
@@ -0,0 +1,191 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+import ICanvasEffect from '../ICanvasEffect';
+
+export type ConfettiOptions = {
+    /**
+     * max confetti count
+     */
+    maxCount: number,
+    /**
+     * particle animation speed
+     */
+    speed: number,
+    /**
+     * the confetti animation frame interval in milliseconds
+     */
+    frameInterval: number,
+    /**
+     * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+     */
+    alpha: number,
+    /**
+     * use gradient instead of solid particle color
+     */
+    gradient: boolean,
+}
+
+type ConfettiParticle = {
+    color: string,
+    color2: string,
+    x: number,
+    y: number,
+    diameter: number,
+    tilt: number,
+    tiltAngleIncrement: number,
+    tiltAngle: number,
+}
+
+export const DefaultOptions: ConfettiOptions = {
+    maxCount: 150,
+    speed: 3,
+    frameInterval: 15,
+    alpha: 1.0,
+    gradient: false,
+};
+
+export default class Confetti implements ICanvasEffect {
+    private readonly options: ConfettiOptions;
+
+    constructor(options: { [key: string]: any }) {
+        this.options = {...DefaultOptions, ...options};
+    }
+
+    private context: CanvasRenderingContext2D | null = null;
+    private supportsAnimationFrame = window.requestAnimationFrame;
+    private colors = ['rgba(30,144,255,', 'rgba(107,142,35,', 'rgba(255,215,0,',
+        'rgba(255,192,203,', 'rgba(106,90,205,', 'rgba(173,216,230,',
+        'rgba(238,130,238,', 'rgba(152,251,152,', 'rgba(70,130,180,',
+        'rgba(244,164,96,', 'rgba(210,105,30,', 'rgba(220,20,60,'];
+
+    private lastFrameTime = Date.now();
+    private particles: Array<ConfettiParticle> = [];
+    private waveAngle = 0;
+
+    public isRunning: boolean;
+
+    public start = async (canvas: HTMLCanvasElement, timeout = 3000) => {
+        if (!canvas) {
+            return;
+        }
+        this.context = canvas.getContext('2d');
+        this.particles = [];
+        const count = this.options.maxCount;
+        while (this.particles.length < count) {
+            this.particles.push(this.resetParticle({} as ConfettiParticle, canvas.width, canvas.height));
+        }
+        this.isRunning = true;
+        this.runAnimation();
+        if (timeout) {
+            window.setTimeout(this.stop, timeout);
+        }
+    }
+
+    public stop = async () => {
+        this.isRunning = false;
+    }
+
+    private resetParticle = (particle: ConfettiParticle, width: number, height: number): ConfettiParticle => {
+        particle.color = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
+        if (this.options.gradient) {
+            particle.color2 = this.colors[(Math.random() * this.colors.length) | 0] + (this.options.alpha + ')');
+        } else {
+            particle.color2 = particle.color;
+        }
+        particle.x = Math.random() * width;
+        particle.y = Math.random() * -height;
+        particle.diameter = Math.random() * 10 + 5;
+        particle.tilt = Math.random() * -10;
+        particle.tiltAngleIncrement = Math.random() * 0.07 + 0.05;
+        particle.tiltAngle = Math.random() * Math.PI;
+        return particle;
+    }
+
+    private runAnimation = (): void => {
+        if (!this.context || !this.context.canvas) {
+            return;
+        }
+        if (this.particles.length === 0) {
+            this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
+        } else {
+            const now = Date.now();
+            const delta = now - this.lastFrameTime;
+            if (!this.supportsAnimationFrame || delta > this.options.frameInterval) {
+                this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
+                this.updateParticles();
+                this.drawParticles(this.context);
+                this.lastFrameTime = now - (delta % this.options.frameInterval);
+            }
+            requestAnimationFrame(this.runAnimation);
+        }
+    }
+
+
+    private drawParticles = (context: CanvasRenderingContext2D): void => {
+        if (!this.context || !this.context.canvas) {
+            return;
+        }
+        let x; let x2; let y2;
+        for (const particle of this.particles) {
+            this.context.beginPath();
+            context.lineWidth = particle.diameter;
+            x2 = particle.x + particle.tilt;
+            x = x2 + particle.diameter / 2;
+            y2 = particle.y + particle.tilt + particle.diameter / 2;
+            if (this.options.gradient) {
+                const gradient = context.createLinearGradient(x, particle.y, x2, y2);
+                gradient.addColorStop(0, particle.color);
+                gradient.addColorStop(1.0, particle.color2);
+                context.strokeStyle = gradient;
+            } else {
+                context.strokeStyle = particle.color;
+            }
+            context.moveTo(x, particle.y);
+            context.lineTo(x2, y2);
+            context.stroke();
+        }
+    }
+
+    private updateParticles = () => {
+        if (!this.context || !this.context.canvas) {
+            return;
+        }
+        const width = this.context.canvas.width;
+        const height = this.context.canvas.height;
+        let particle: ConfettiParticle;
+        this.waveAngle += 0.01;
+        for (let i = 0; i < this.particles.length; i++) {
+            particle = this.particles[i];
+            if (!this.isRunning && particle.y < -15) {
+                particle.y = height + 100;
+            } else {
+                particle.tiltAngle += particle.tiltAngleIncrement;
+                particle.x += Math.sin(this.waveAngle) - 0.5;
+                particle.y += (Math.cos(this.waveAngle) + particle.diameter + this.options.speed) * 0.5;
+                particle.tilt = Math.sin(particle.tiltAngle) * 15;
+            }
+            if (particle.x > width + 20 || particle.x < -20 || particle.y > height) {
+                if (this.isRunning && this.particles.length <= this.options.maxCount) {
+                    this.resetParticle(particle, width, height);
+                } else {
+                    this.particles.splice(i, 1);
+                    i--;
+                }
+            }
+        }
+    }
+}
diff --git a/src/effects/index.ts b/src/effects/index.ts
new file mode 100644
index 0000000000..16a0851070
--- /dev/null
+++ b/src/effects/index.ts
@@ -0,0 +1,89 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+import { _t, _td } from "../languageHandler";
+
+export type Effect<TOptions extends { [key: string]: any }> = {
+    /**
+     * one or more emojis that will trigger this effect
+     */
+    emojis: Array<string>;
+    /**
+     * the matrix message type that will trigger this effect
+     */
+    msgType: string;
+    /**
+     * the room command to trigger this effect
+     */
+    command: string;
+    /**
+     * a function that returns the translated description of the effect
+     */
+    description: () => string;
+    /**
+     * a function that returns the translated fallback message. this message will be shown if the user did not provide a custom message
+     */
+    fallbackMessage: () => string;
+    /**
+     * animation options
+     */
+    options: TOptions;
+}
+
+type ConfettiOptions = {
+    /**
+     * max confetti count
+     */
+    maxCount: number,
+    /**
+     * particle animation speed
+     */
+    speed: number,
+    /**
+     * the confetti animation frame interval in milliseconds
+     */
+    frameInterval: number,
+    /**
+     * the alpha opacity of the confetti (between 0 and 1, where 1 is opaque and 0 is invisible)
+     */
+    alpha: number,
+    /**
+     * use gradient instead of solid particle color
+     */
+    gradient: boolean,
+}
+
+/**
+ * This configuration defines room effects that can be triggered by custom message types and emojis
+ */
+export const CHAT_EFFECTS: Array<Effect<{ [key: string]: any }>> = [
+    {
+        emojis: ['🎊', '🎉'],
+        msgType: 'nic.custom.confetti',
+        command: 'confetti',
+        description: () => _td("Sends the given message with confetti"),
+        fallbackMessage: () => _t("sends confetti") + " 🎉",
+        options: {
+            maxCount: 150,
+            speed: 3,
+            frameInterval: 15,
+            alpha: 1.0,
+            gradient: false,
+        },
+    } as Effect<ConfettiOptions>,
+];
+
+
diff --git a/src/effects/utils.ts b/src/effects/utils.ts
new file mode 100644
index 0000000000..c2b499b154
--- /dev/null
+++ b/src/effects/utils.ts
@@ -0,0 +1,24 @@
+/*
+ Copyright 2020 Nurjin Jafar
+ Copyright 2020 Nordeck IT + Consulting GmbH.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+/**
+ * Checks a message if it contains one of the provided emojis
+ * @param  {Object} content The message
+ * @param  {Array<string>} emojis The list of emojis to check for
+ */
+export const containsEmoji = (content: { msgtype: string, body: string }, emojis: Array<string>): boolean => {
+    return emojis.some((emoji) => content.body && content.body.includes(emoji));
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4ef720da65..1b5d4b6ec4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -406,6 +406,7 @@
     "Messages": "Messages",
     "Actions": "Actions",
     "Advanced": "Advanced",
+    "Effects": "Effects",
     "Other": "Other",
     "Command error": "Command error",
     "Usage": "Usage",
@@ -826,6 +827,7 @@
     "Manually verify all remote sessions": "Manually verify all remote sessions",
     "IRC display name width": "IRC display name width",
     "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
+    "Show chat effects": "Show chat effects",
     "Collecting app version information": "Collecting app version information",
     "Collecting logs": "Collecting logs",
     "Uploading logs": "Uploading logs",
@@ -844,6 +846,8 @@
     "When rooms are upgraded": "When rooms are upgraded",
     "My Ban List": "My Ban List",
     "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
+    "Sends the given message with confetti": "Sends the given message with confetti",
+    "sends confetti": "sends confetti",
     "Video Call": "Video Call",
     "Voice Call": "Voice Call",
     "Fill Screen": "Fill Screen",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 31e133be72..6bec31a1cb 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -634,6 +634,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Enable experimental, compact IRC style layout"),
         default: false,
     },
+    "showChatEffects": {
+        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+        displayName: _td("Show chat effects"),
+        default: true,
+    },
     "Widgets.pinned": {
         supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
         default: {},