diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..44610e5
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake;
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2bbdbfe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.direnv
+result
diff --git a/README.md b/README.md
index 14d339b..815b72e 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,14 @@
# tachikoma
-Patchset for Akkoma
\ No newline at end of file
+Patchset for Akkoma
+
+## patchlists
+
+### akkoma (backend)
+
+_none_
+
+### akkoma-fe (frontend)
+
+- 0001-confirm-favorite: opens a "confirmation" modal on favorites, identical to reposts. (TODO: undo formatting changes)
+- 0002-move-notifications: moves toast notifications to the bottom of the screen
diff --git a/akkoma-fe/0001-confirm-favorite.patch b/akkoma-fe/0001-confirm-favorite.patch
new file mode 100644
index 0000000..52de84d
--- /dev/null
+++ b/akkoma-fe/0001-confirm-favorite.patch
@@ -0,0 +1,144 @@
+diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
+index d15699f7..e5f1a3c5 100644
+--- a/src/components/favorite_button/favorite_button.js
++++ b/src/components/favorite_button/favorite_button.js
+@@ -1,41 +1,60 @@
+-import { mapGetters } from 'vuex'
+-import { library } from '@fortawesome/fontawesome-svg-core'
+-import { faStar } from '@fortawesome/free-solid-svg-icons'
+-import {
+- faStar as faStarRegular
+-} from '@fortawesome/free-regular-svg-icons'
++import ConfirmModal from "../confirm_modal/confirm_modal.vue";
++import { mapGetters } from "vuex";
++import { library } from "@fortawesome/fontawesome-svg-core";
++import { faStar } from "@fortawesome/free-solid-svg-icons";
++import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
+
+-library.add(
+- faStar,
+- faStarRegular
+-)
++library.add(faStar, faStarRegular);
+
+ const FavoriteButton = {
+- props: ['status', 'loggedIn'],
+- data () {
++ props: ["status", "loggedIn"],
++ components: {
++ ConfirmModal,
++ },
++ data() {
+ return {
+- animated: false
+- }
++ animated: false,
++ showingConfirmDialog: false,
++ };
+ },
+ methods: {
+- favorite () {
++ favorite() {
++ if (!this.status.favorited && this.shouldConfirmFavorite) {
++ this.showConfirmDialog();
++ } else {
++ this.doFavorite();
++ }
++ },
++ doFavorite() {
+ if (!this.status.favorited) {
+- this.$store.dispatch('favorite', { id: this.status.id })
++ this.$store.dispatch("favorite", { id: this.status.id });
+ } else {
+- this.$store.dispatch('unfavorite', { id: this.status.id })
++ this.$store.dispatch("unfavorite", { id: this.status.id });
+ }
+- this.animated = true
++ this.animated = true;
+ setTimeout(() => {
+- this.animated = false
+- }, 500)
+- }
++ this.animated = false;
++ }, 500);
++ this.hideConfirmDialog();
++ },
++ showConfirmDialog() {
++ this.showingConfirmDialog = true;
++ },
++ hideConfirmDialog() {
++ this.showingConfirmDialog = false;
++ },
+ },
+ computed: {
+- ...mapGetters(['mergedConfig']),
+- remoteInteractionLink () {
+- return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
+- }
+- }
+-}
++ ...mapGetters(["mergedConfig"]),
++ shouldConfirmFavorite() {
++ return this.mergedConfig.modalOnFavorite;
++ },
++ remoteInteractionLink() {
++ return this.$store.getters.remoteInteractionLink({
++ statusId: this.status.id,
++ });
++ },
++ },
++};
+
+-export default FavoriteButton
++export default FavoriteButton;
+diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
+index 16bf441e..06ed7d59 100644
+--- a/src/components/favorite_button/favorite_button.vue
++++ b/src/components/favorite_button/favorite_button.vue
+@@ -32,6 +32,18 @@
+ >
+ {{ status.fave_num }}
+
++
++
++ {{ $t('status.favorite_confirm') }}
++
++
+
+
+
+diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
+index 64950f8a..b09c6d9f 100644
+--- a/src/components/settings_modal/tabs/general_tab.vue
++++ b/src/components/settings_modal/tabs/general_tab.vue
+@@ -282,6 +282,11 @@
+ {{ $t('settings.confirm_dialogs_repeat') }}
+
+
++
++
++ {{ $t('settings.confirm_dialogs_favorite') }}
++
++
+
+
+ {{ $t('settings.confirm_dialogs_unfollow') }}
+diff --git a/src/modules/config.js b/src/modules/config.js
+index 551b5bb6..0e78c749 100644
+--- a/src/modules/config.js
++++ b/src/modules/config.js
+@@ -83,6 +83,7 @@ export const defaultState = {
+ // This hides statuses filtered via a word filter
+ hideFilteredStatuses: undefined, // instance default
+ modalOnRepeat: undefined, // instance default
++ modalOnFavorite: undefined, // instance default
+ modalOnUnfollow: undefined, // instance default
+ modalOnBlock: undefined, // instance default
+ modalOnMute: undefined, // instance default
diff --git a/akkoma-fe/0002-move-notifications.patch b/akkoma-fe/0002-move-notifications.patch
new file mode 100644
index 0000000..48cc26d
--- /dev/null
+++ b/akkoma-fe/0002-move-notifications.patch
@@ -0,0 +1,13 @@
+diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue
+index ddc45b81..492fc4b6 100644
+--- a/src/components/global_notice_list/global_notice_list.vue
++++ b/src/components/global_notice_list/global_notice_list.vue
+@@ -29,7 +29,7 @@
+
+ .global-notice-list {
+ position: fixed;
+- top: 50px;
++ bottom: 5px;
+ width: 100%;
+ pointer-events: none;
+ z-index: 1001;
diff --git a/akkoma-fe/upstream.txt b/akkoma-fe/upstream.txt
new file mode 100644
index 0000000..30eb958
--- /dev/null
+++ b/akkoma-fe/upstream.txt
@@ -0,0 +1 @@
+7cc6c3565466b330043e0a811a6e1e2db487ec8d
\ No newline at end of file
diff --git a/akkoma/0000.patch b/akkoma/0000.patch
new file mode 100644
index 0000000..e69de29
diff --git a/akkoma/upstream.txt b/akkoma/upstream.txt
new file mode 100644
index 0000000..141dcc0
--- /dev/null
+++ b/akkoma/upstream.txt
@@ -0,0 +1 @@
+26a91d5c9eef1b2c75d3e0e7cc792d62e953cb30
\ No newline at end of file
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..90150d5
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1712388808,
+ "narHash": "sha256-9ogU4c3vUmuMDoRlbQCeq3OKx0XJmgHcLZ4XywJNYWI=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "fe4295b9ecd88764c1abf6179e03b1a828ca0e9a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..60d20eb
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,81 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
+ };
+
+ outputs = { self, nixpkgs, ...} @ inputs: let
+ systems = [
+ "aarch64-linux"
+ "x86_64-linux"
+ ];
+ forAllSystems = nixpkgs.lib.genAttrs systems;
+ in rec {
+ devShells = forAllSystems (system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in {
+ default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ just
+ ];
+ };
+ });
+
+ nixosModules = rec {
+ default = enableTachikoma;
+ enableTachikoma = { pkgs, ...}: {
+ services.akkoma.package = packages.${pkgs.system}.akkoma.tachikoma;
+ services.akkoma.frontends.primary = {
+ name = "tachikoma-fe";
+ ref = "stable";
+ package = packages.${pkgs.system}.akkoma-frontends.tachikoma-fe;
+ };
+ };
+ };
+
+ packages = forAllSystems (system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ lib = pkgs.lib;
+ in rec {
+ default = akkoma.tachikoma;
+
+ akkoma.tachikoma = let
+ rev = builtins.readFile ./akkoma/upstream.txt;
+ in pkgs.akkoma.overrideAttrs (final: prev: {
+ version = "${rev}+tachikoma";
+
+ src = pkgs.fetchFromGitea {
+ inherit rev;
+ domain = "akkoma.dev";
+ owner = "AkkomaGang";
+ repo = "akkoma";
+ hash = "sha256-eKvfuHTLmUU6Dom/GctPSbhrAAik1T/7bYY5j3YUkRo=";
+ };
+
+ patches = [
+ # none yet
+ ];
+ patchFlags = "-p1 -F5";
+ });
+
+ akkoma-frontends.tachikoma-fe = let
+ rev = builtins.readFile ./akkoma-fe/upstream.txt;
+ in pkgs.akkoma-frontends.akkoma-fe.overrideAttrs (final: prev: {
+ version = "${rev}+tachikoma";
+
+ src = pkgs.fetchFromGitea {
+ inherit rev;
+ domain = "akkoma.dev";
+ owner = "AkkomaGang";
+ repo = "akkoma-fe";
+ hash = "sha256-Z7psmIyOo8Rvwcip90JgxLhZ5SkkGB94QInEgm8UOjQ=";
+ };
+
+ patches = [
+ ./akkoma-fe/0001-confirm-favorite.patch
+ ./akkoma-fe/0002-move-notifications.patch
+ ];
+ patchFlags = "-p1 -F5";
+ });
+ });
+ };
+}
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..646681d
--- /dev/null
+++ b/justfile
@@ -0,0 +1,8 @@
+default: build
+build: build-be build-fe
+
+build-be:
+ nix build .#akkoma.tachikoma
+
+build-fe:
+ nix build .#akkoma-frontends.tachikoma-fe
\ No newline at end of file