├── .watchmanconfig ├── .node-version ├── .ruby-version ├── index.js ├── mise.toml ├── index.share.js ├── .bundle └── config ├── bun.lockb ├── .yarnrc.yml ├── .eslintrc.js ├── src ├── assets │ └── icons │ │ ├── add.png │ │ ├── rtt.png │ │ ├── web.png │ │ ├── add@2x.png │ │ ├── add@3x.png │ │ ├── block.png │ │ ├── code.png │ │ ├── group.png │ │ ├── link.png │ │ ├── more.png │ │ ├── reply.png │ │ ├── report.png │ │ ├── rtt@2x.png │ │ ├── rtt@3x.png │ │ ├── web@2x.png │ │ ├── web@3x.png │ │ ├── block@2x.png │ │ ├── block@3x.png │ │ ├── code@2x.png │ │ ├── code@3x.png │ │ ├── group@2x.png │ │ ├── group@3x.png │ │ ├── link@2x.png │ │ ├── link@3x.png │ │ ├── more@2x.png │ │ ├── more@3x.png │ │ ├── more_ios.png │ │ ├── nav │ │ ├── help.png │ │ ├── help@2x.png │ │ ├── help@3x.png │ │ ├── replies.png │ │ ├── bookmarks.png │ │ ├── discover.png │ │ ├── mentions.png │ │ ├── timeline.png │ │ ├── bookmarks@2x.png │ │ ├── bookmarks@3x.png │ │ ├── discover@2x.png │ │ ├── discover@3x.png │ │ ├── mentions@2x.png │ │ ├── mentions@3x.png │ │ ├── replies@2x.png │ │ ├── replies@3x.png │ │ ├── timeline@2x.png │ │ └── timeline@3x.png │ │ ├── post_add.png │ │ ├── reply@2x.png │ │ ├── reply@3x.png │ │ ├── add_account.png │ │ ├── arrow_back.png │ │ ├── checkmark.png │ │ ├── more_ios@2x.png │ │ ├── more_ios@3x.png │ │ ├── more_white.png │ │ ├── post_add@2x.png │ │ ├── post_add@3x.png │ │ ├── report@2x.png │ │ ├── report@3x.png │ │ ├── arrow_back@2x.png │ │ ├── arrow_back@3x.png │ │ ├── checkmark@2x.png │ │ ├── checkmark@3x.png │ │ ├── more_white@2x.png │ │ ├── more_white@3x.png │ │ ├── add_account@2x.png │ │ ├── add_account@3x.png │ │ ├── more_ios_white.png │ │ ├── tab_bar │ │ ├── discover.png │ │ ├── mentions.png │ │ ├── timeline.png │ │ ├── discover@2x.png │ │ ├── discover@3x.png │ │ ├── mentions@2x.png │ │ ├── mentions@3x.png │ │ ├── timeline@2x.png │ │ └── timeline@3x.png │ │ ├── toolbar │ │ ├── settings.png │ │ ├── photo_library.png │ │ ├── settings@2x.png │ │ ├── settings@3x.png │ │ ├── photo_library@2x.png │ │ └── photo_library@3x.png │ │ ├── more_ios_white@2x.png │ │ └── more_ios_white@3x.png ├── stores │ ├── Collection.js │ ├── models │ │ ├── posting │ │ │ ├── Contact.js │ │ │ ├── Page.js │ │ │ └── Post.js │ │ ├── Device.js │ │ ├── Token.js │ │ └── Notification.js │ ├── enums │ │ └── blog_services.js │ ├── Settings.js │ ├── Reporting.js │ ├── Replies.js │ └── Discover.js ├── utils │ ├── ui.js │ ├── string_checker.js │ ├── string_utils.js │ ├── dev.js │ └── snapshots.js ├── index.js ├── index.share.js ├── components │ ├── text │ │ └── highlighting_text.js │ ├── info │ │ └── login_message.js │ ├── share │ │ ├── dev.js │ │ └── header.js │ ├── sheets │ │ ├── header.js │ │ ├── login_message.js │ │ ├── sheets.js │ │ ├── notifications.js │ │ ├── menu.js │ │ ├── tagmoji.js │ │ └── posts_destination.js │ ├── web │ │ ├── loading_view.js │ │ └── error_view.js │ ├── header │ │ ├── update_reply.js │ │ ├── post_button.js │ │ ├── post_reply.js │ │ ├── update_page.js │ │ ├── screen_title.js │ │ ├── update_post.js │ │ ├── new_collection.js │ │ ├── refresh_activity.js │ │ ├── remove_image.js │ │ ├── add_bookmark.js │ │ ├── new_post.js │ │ ├── close.js │ │ ├── close_post_clear.js │ │ ├── reply.js │ │ ├── profile_image.js │ │ ├── new_upload.js │ │ └── back.js │ ├── generic │ │ ├── loading.js │ │ └── generic_screen.js │ ├── common │ │ └── MBImage.js │ ├── cells │ │ └── checkmark_row_cell.js │ ├── images │ │ └── image_modal.js │ ├── tabs │ │ └── tab.js │ ├── search_bar.js │ ├── settings │ │ ├── user_muting.js │ │ └── user_posting.js │ ├── bookmarks │ │ └── tag_filter_header.js │ └── keyboard │ │ └── username_toolbar.js └── screens │ ├── loading │ └── Loading.js │ ├── bookmarks │ ├── bookmark.js │ ├── bookmarks.js │ └── add_bookmark.js │ ├── discover │ ├── topic.js │ └── discover.js │ ├── mentions │ └── mentions.js │ ├── timeline │ └── timeline.js │ ├── following │ └── following.js │ ├── conversation │ └── conversation.js │ ├── profile │ └── profile.js │ ├── stacks │ ├── MentionsStack.js │ ├── TimelineStack.js │ ├── PostEditStack.js │ ├── DiscoverStack.js │ ├── BookmarksStack.js │ ├── TabNavigator.js │ └── PostingStack.js │ ├── replies │ ├── edit.android.js │ ├── replies.js │ └── edit.ios.js │ └── share │ ├── post.android.js │ ├── index.android.js │ ├── post.ios.js │ └── index.js ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_adaptive_back.png │ │ │ │ │ └── ic_launcher_adaptive_fore.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── ic_notification.png │ │ │ │ │ └── shell_launch_background_image.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── values-night │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ ├── splash_background.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ └── java │ │ │ │ └── blog │ │ │ │ └── micro │ │ │ │ └── android │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── release │ │ │ └── java │ │ │ └── blog │ │ │ └── micro │ │ │ └── android │ │ │ └── ReactNativeFlipper.java │ └── proguard-rules.pro ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle ├── settings.gradle └── gradle.properties ├── jsconfig.json ├── ios ├── MicroBlog_RN │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── icon-152.png │ │ │ ├── icon-167.png │ │ │ ├── icon-1024 1.png │ │ │ ├── icon-1024 2.png │ │ │ ├── icon-1024.png │ │ │ ├── icon-152 1.png │ │ │ ├── icon-152 2.png │ │ │ ├── icon-167 1.png │ │ │ └── icon-167 2.png │ │ ├── icon_512x512.imageset │ │ │ ├── icon_512x512.png │ │ │ └── Contents.json │ │ ├── color_syntax_tags.colorset │ │ │ └── Contents.json │ │ └── color_editing_paragraph.colorset │ │ │ └── Contents.json │ ├── UIBarButtonItem+Plainify.h │ ├── UIScrollViewDelegate+Zooming.h │ ├── MBClipboardHelper.h │ ├── MBHighlightingTextStorage.h │ ├── RCTInputAccessoryShadowView.h │ ├── UIScrollViewDelegate+Zooming.m │ ├── MicroBlog_RN.entitlements │ ├── RCTInputAccessoryShadowView.m │ ├── MBClipboardHelper.m │ ├── MBHighlightingTextManager.h │ ├── MBHighlightingTextView.h │ ├── UIBarButtonItem+Plainify.m │ └── Info.plist ├── File.swift ├── MicroBlog_RN-Bridging-Header.h ├── MicroBlog_RN.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MicroBlog_Share │ ├── MicroBlog_Share-Bridging-Header.h │ ├── MicroBlog_Share.entitlements │ ├── GetURL.js │ ├── Info.plist │ └── Base.lproj │ │ └── MainInterface.storyboard ├── ci_scripts │ └── ci_post_clone.sh ├── .xcode.env ├── PrivacyInfo.xcprivacy └── Podfile ├── app.json ├── babel.config.js ├── Gemfile ├── metro.config.js ├── patches ├── react-native-fs+2.20.0.patch ├── react-native-image-viewing+0.2.2.patch └── react-native-sensitive-info@5.5.8.patch ├── LICENSE ├── .gitignore └── Gemfile.lock /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.4.5 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./src/index.js') -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | ruby = "3.4.5" 3 | -------------------------------------------------------------------------------- /index.share.js: -------------------------------------------------------------------------------- 1 | require('./src/index.share.js') -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/bun.lockb -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.4.cjs -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native', 4 | }; 5 | -------------------------------------------------------------------------------- /src/assets/icons/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/add.png -------------------------------------------------------------------------------- /src/assets/icons/rtt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/rtt.png -------------------------------------------------------------------------------- /src/assets/icons/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/web.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /src/assets/icons/add@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/add@2x.png -------------------------------------------------------------------------------- /src/assets/icons/add@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/add@3x.png -------------------------------------------------------------------------------- /src/assets/icons/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/block.png -------------------------------------------------------------------------------- /src/assets/icons/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/code.png -------------------------------------------------------------------------------- /src/assets/icons/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/group.png -------------------------------------------------------------------------------- /src/assets/icons/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/link.png -------------------------------------------------------------------------------- /src/assets/icons/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more.png -------------------------------------------------------------------------------- /src/assets/icons/reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/reply.png -------------------------------------------------------------------------------- /src/assets/icons/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/report.png -------------------------------------------------------------------------------- /src/assets/icons/rtt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/rtt@2x.png -------------------------------------------------------------------------------- /src/assets/icons/rtt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/rtt@3x.png -------------------------------------------------------------------------------- /src/assets/icons/web@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/web@2x.png -------------------------------------------------------------------------------- /src/assets/icons/web@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/web@3x.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "allowJs": true 5 | } 6 | } -------------------------------------------------------------------------------- /src/assets/icons/block@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/block@2x.png -------------------------------------------------------------------------------- /src/assets/icons/block@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/block@3x.png -------------------------------------------------------------------------------- /src/assets/icons/code@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/code@2x.png -------------------------------------------------------------------------------- /src/assets/icons/code@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/code@3x.png -------------------------------------------------------------------------------- /src/assets/icons/group@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/group@2x.png -------------------------------------------------------------------------------- /src/assets/icons/group@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/group@3x.png -------------------------------------------------------------------------------- /src/assets/icons/link@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/link@2x.png -------------------------------------------------------------------------------- /src/assets/icons/link@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/link@3x.png -------------------------------------------------------------------------------- /src/assets/icons/more@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more@2x.png -------------------------------------------------------------------------------- /src/assets/icons/more@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more@3x.png -------------------------------------------------------------------------------- /src/assets/icons/more_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_ios.png -------------------------------------------------------------------------------- /src/assets/icons/nav/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/help.png -------------------------------------------------------------------------------- /src/assets/icons/post_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/post_add.png -------------------------------------------------------------------------------- /src/assets/icons/reply@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/reply@2x.png -------------------------------------------------------------------------------- /src/assets/icons/reply@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/reply@3x.png -------------------------------------------------------------------------------- /src/assets/icons/add_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/add_account.png -------------------------------------------------------------------------------- /src/assets/icons/arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/arrow_back.png -------------------------------------------------------------------------------- /src/assets/icons/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/checkmark.png -------------------------------------------------------------------------------- /src/assets/icons/more_ios@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_ios@2x.png -------------------------------------------------------------------------------- /src/assets/icons/more_ios@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_ios@3x.png -------------------------------------------------------------------------------- /src/assets/icons/more_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_white.png -------------------------------------------------------------------------------- /src/assets/icons/nav/help@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/help@2x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/help@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/help@3x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/replies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/replies.png -------------------------------------------------------------------------------- /src/assets/icons/post_add@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/post_add@2x.png -------------------------------------------------------------------------------- /src/assets/icons/post_add@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/post_add@3x.png -------------------------------------------------------------------------------- /src/assets/icons/report@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/report@2x.png -------------------------------------------------------------------------------- /src/assets/icons/report@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/report@3x.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/icons/arrow_back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/arrow_back@2x.png -------------------------------------------------------------------------------- /src/assets/icons/arrow_back@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/arrow_back@3x.png -------------------------------------------------------------------------------- /src/assets/icons/checkmark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/checkmark@2x.png -------------------------------------------------------------------------------- /src/assets/icons/checkmark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/checkmark@3x.png -------------------------------------------------------------------------------- /src/assets/icons/more_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_white@2x.png -------------------------------------------------------------------------------- /src/assets/icons/more_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_white@3x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/bookmarks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/bookmarks.png -------------------------------------------------------------------------------- /src/assets/icons/nav/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/discover.png -------------------------------------------------------------------------------- /src/assets/icons/nav/mentions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/mentions.png -------------------------------------------------------------------------------- /src/assets/icons/nav/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/timeline.png -------------------------------------------------------------------------------- /src/assets/icons/add_account@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/add_account@2x.png -------------------------------------------------------------------------------- /src/assets/icons/add_account@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/add_account@3x.png -------------------------------------------------------------------------------- /src/assets/icons/more_ios_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_ios_white.png -------------------------------------------------------------------------------- /src/assets/icons/nav/bookmarks@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/bookmarks@2x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/bookmarks@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/bookmarks@3x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/discover@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/discover@2x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/discover@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/discover@3x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/mentions@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/mentions@2x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/mentions@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/mentions@3x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/replies@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/replies@2x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/replies@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/replies@3x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/timeline@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/timeline@2x.png -------------------------------------------------------------------------------- /src/assets/icons/nav/timeline@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/nav/timeline@3x.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/discover.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/mentions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/mentions.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/timeline.png -------------------------------------------------------------------------------- /src/assets/icons/toolbar/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/toolbar/settings.png -------------------------------------------------------------------------------- /src/assets/icons/more_ios_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_ios_white@2x.png -------------------------------------------------------------------------------- /src/assets/icons/more_ios_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/more_ios_white@3x.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/discover@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/discover@2x.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/discover@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/discover@3x.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/mentions@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/mentions@2x.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/mentions@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/mentions@3x.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/timeline@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/timeline@2x.png -------------------------------------------------------------------------------- /src/assets/icons/tab_bar/timeline@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/tab_bar/timeline@3x.png -------------------------------------------------------------------------------- /src/assets/icons/toolbar/photo_library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/toolbar/photo_library.png -------------------------------------------------------------------------------- /src/assets/icons/toolbar/settings@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/toolbar/settings@2x.png -------------------------------------------------------------------------------- /src/assets/icons/toolbar/settings@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/toolbar/settings@3x.png -------------------------------------------------------------------------------- /ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // MicroBlog_RN 4 | // 5 | // Created by Vincent Ritter on 12/10/2021. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /src/assets/icons/toolbar/photo_library@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/toolbar/photo_library@2x.png -------------------------------------------------------------------------------- /src/assets/icons/toolbar/photo_library@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/src/assets/icons/toolbar/photo_library@3x.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f80 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Micro.blog", 3 | "displayName": "Micro.blog", 4 | "plugins": [ 5 | "expo-secure-store", 6 | "expo-web-browser" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Micro.blog 3 | #fff 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-152.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-167.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-1024 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-1024 1.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-1024 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-1024 2.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-152 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-152 1.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-152 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-152 2.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-167 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-167 1.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-167 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/AppIcon.appiconset/icon-167 2.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/icon_512x512.imageset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/ios/MicroBlog_RN/Images.xcassets/icon_512x512.imageset/icon_512x512.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/shell_launch_background_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microdotblog/microblog-react/HEAD/android/app/src/main/res/drawable-xxxhdpi/shell_launch_background_image.png -------------------------------------------------------------------------------- /ios/MicroBlog_RN-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | -------------------------------------------------------------------------------- /src/stores/Collection.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | 3 | export default Collection = types.model('Collection', { 4 | id: types.identifierNumber, 5 | name: types.string, 6 | uploads_count: types.optional(types.number, 0) 7 | }); -------------------------------------------------------------------------------- /src/stores/models/posting/Contact.js: -------------------------------------------------------------------------------- 1 | import { types, flow } from 'mobx-state-tree'; 2 | 3 | export default Contact = types.model('Contact', { 4 | username: types.maybe(types.string), 5 | avatar: types.maybe(types.string) 6 | }) 7 | .actions(self => ({ 8 | })) -------------------------------------------------------------------------------- /src/utils/ui.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | export const STANDARD_SLOP = 7; 4 | 5 | export function isLiquidGlass() { 6 | return ( 7 | false 8 | // (Platform.OS == 'ios') && (parseInt(Platform.Version, 10) >= 26) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/stores/models/Device.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | 3 | export default Device = types.model('Device', { 4 | token: types.identifier, 5 | app_name: types.maybe(types.string), 6 | push_env: types.maybe(types.string), 7 | created_at: types.maybe(types.string), 8 | }) 9 | -------------------------------------------------------------------------------- /src/stores/models/Token.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | 3 | export default Token = types.model('Token', { 4 | token: types.identifier, 5 | username: types.maybe(types.string), 6 | type: types.optional(types.string, "user"), 7 | service_id: types.maybe(types.string), 8 | }) 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['babel-preset-expo'], 3 | plugins: [ 4 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 5 | 'react-native-reanimated/plugin' 6 | ], 7 | env: { 8 | production: { 9 | plugins: ['transform-remove-console'] 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/UIBarButtonItem+Plainify.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+Plainify.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 8/30/25. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface UIBarButtonItem (Plainify) 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/UIScrollViewDelegate+Zooming.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollViewDelegate+Zooming.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 4/27/23. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface NSObject (Zooming) 13 | 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import MainApp from './screens/App'; 3 | import { name as appName } from './../app.json'; 4 | import './utils/dev'; 5 | import './utils/string_checker'; 6 | import './utils/snapshots'; 7 | import './utils/string_utils'; 8 | 9 | AppRegistry.registerComponent(appName, () => MainApp); -------------------------------------------------------------------------------- /src/index.share.js: -------------------------------------------------------------------------------- 1 | import './utils/dev'; 2 | import './utils/string_checker'; 3 | import './utils/string_utils'; 4 | import './utils/snapshots'; 5 | import { AppRegistry } from "react-native" 6 | import ShareScreen from "./screens/share" 7 | 8 | AppRegistry.registerComponent( 9 | "ShareMenuModuleComponent", () => ShareScreen 10 | ) -------------------------------------------------------------------------------- /ios/MicroBlog_Share/MicroBlog_Share-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | -------------------------------------------------------------------------------- /ios/ci_scripts/ci_post_clone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # What are we running on? 4 | system_profiler SPHardwareDataType 5 | 6 | # Install CocoaPods and bun. 7 | brew tap oven-sh/bun 8 | brew install cocoapods node bun 9 | 10 | bun install 11 | 12 | npm config set maxsockets 3 13 | 14 | # Install dependencies you manage with CocoaPods. 15 | bun pods -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/MBClipboardHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // MBClipboardHelper.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 6/13/25. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MBClipboardHelper : NSObject 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/MBHighlightingTextStorage.h: -------------------------------------------------------------------------------- 1 | // 2 | // MBHighlightingTextStorage.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 4/16/23. 6 | // From Micro.blog 2.x iOS code, which was based on TKDHighlightingTextStorage by Max Seelemann. 7 | // 8 | 9 | #import 10 | 11 | @interface MBHighlightingTextStorage : NSTextStorage 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/RCTInputAccessoryShadowView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RCTInputAccessoryShadowView.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 10/24/23. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface RCTInputAccessoryShadowView (ScreenWidth) 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '<= 1.3.4' 11 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/UIScrollViewDelegate+Zooming.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollViewDelegate+Zooming.m 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 4/27/23. 6 | // 7 | 8 | #import "UIScrollViewDelegate+Zooming.h" 9 | 10 | @implementation NSObject (Zooming) 11 | 12 | - (void) scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view 13 | { 14 | scrollView.pinchGestureRecognizer.enabled = NO; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/icon_512x512.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "icon_512x512.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/text/highlighting_text.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { requireNativeComponent, Platform, TextInput } from 'react-native'; 3 | 4 | const MBHighlightingTextView = requireNativeComponent("MBHighlightingTextView"); 5 | 6 | export default class HighlightingText extends React.Component { 7 | render() { 8 | if(Platform.OS === "ios"){ 9 | return( ) 10 | } 11 | return( ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /ios/MicroBlog_Share/MicroBlog_Share.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.blog.micro.ios 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)blog.micro.ios 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ios/MicroBlog_Share/GetURL.js: -------------------------------------------------------------------------------- 1 | var MyExtensionJavaScriptClass = function() {}; 2 | 3 | MyExtensionJavaScriptClass.prototype = { 4 | run: function(arguments) { 5 | // Pass the baseURI of the webpage to the extension. 6 | arguments.completionFunction({"title": document.title, "url": document.URL, "text": document.getSelection().toString()}); 7 | } 8 | }; 9 | 10 | // The JavaScript file must contain a global object named "ExtensionPreprocessingJS". 11 | var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass; 12 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /src/stores/enums/blog_services.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | 3 | export const blog_services = { 4 | microblog: { name: "Micro.blog", url: "https://micro.blog/micropub", type: "micropub", description: "Micro.blog hosted blog" }, 5 | xmlrpc: { name: "WordPress", url: "", type: "xmlrpc", description: "WordPress or compatible blog" }, 6 | micropub: { name: "Micropub", url: "", type: "micropub", description: "WordPress or compatible blog" }, 7 | }; 8 | 9 | export default types.enumeration('BlogServices', Object.keys(blog_services)); 10 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/MicroBlog_RN.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.security.application-groups 8 | 9 | group.blog.micro.ios 10 | 11 | keychain-access-groups 12 | 13 | $(AppIdentifierPrefix)blog.micro.ios 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/RCTInputAccessoryShadowView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RCTInputAccessoryShadowView.m 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 10/24/23. 6 | // 7 | 8 | #import "RCTInputAccessoryShadowView.h" 9 | 10 | @implementation RCTInputAccessoryShadowView (ScreenWidth) 11 | 12 | - (void) insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex 13 | { 14 | [super insertReactSubview:subview atIndex:atIndex]; 15 | 16 | CGRect r = [[UIScreen mainScreen] bounds]; 17 | subview.width = (YGValue) { r.size.width, YGUnitPoint }; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config'); 2 | const { mergeConfig } = require('@react-native/metro-config'); 3 | 4 | /** 5 | * Metro configuration 6 | * https://reactnative.dev/docs/metro 7 | * 8 | * @type {import('metro-config').MetroConfig} 9 | */ 10 | const config = { 11 | transformer: { 12 | getTransformOptions: async () => ({ 13 | transform: { 14 | experimentalImportSupport: false, 15 | inlineRequires: true, 16 | }, 17 | }), 18 | }, 19 | }; 20 | 21 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 22 | -------------------------------------------------------------------------------- /src/screens/loading/Loading.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Text, View } from 'react-native'; 4 | import App from '../../stores/App' 5 | 6 | @observer 7 | export default class LoadingScreen extends React.Component { 8 | 9 | componentDidMount() { 10 | //App.set_navigation(this.props.navigation) 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | Loading... 17 | 18 | ) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/utils/string_checker.js: -------------------------------------------------------------------------------- 1 | class StringChecker { 2 | 3 | _validate_email = (value) => { 4 | var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 5 | return re.test(value); 6 | } 7 | 8 | _validate_is_token = (value) => { 9 | const is_email = this._validate_email(value) 10 | return !is_email && value.length === 20 11 | } 12 | 13 | _validate_url = (value) => { 14 | var re = /^(https?:\/\/)?([a-z0-9-]+\.)+[a-z]{2,}(\/[^\s]*)?(\?\S*)?(#[^\s]*)?$/ 15 | return re.test(value); 16 | } 17 | 18 | } 19 | export default new StringChecker() -------------------------------------------------------------------------------- /ios/MicroBlog_RN/MBClipboardHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // MBClipboardHelper.m 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 6/13/25. 6 | // 7 | 8 | #import "MBClipboardHelper.h" 9 | 10 | @implementation MBClipboardHelper 11 | 12 | RCT_EXPORT_MODULE(MBClipboardHelper); 13 | 14 | // expose hasWebURL(): Promise 15 | RCT_REMAP_METHOD(hasWebURL, resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) 16 | { 17 | @try { 18 | BOOL has_urls = [UIPasteboard generalPasteboard].hasURLs; 19 | resolve(@(has_urls)); 20 | } 21 | @catch (NSException* exception) { 22 | reject(@"pasteboard_error", exception.reason, nil); 23 | } 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /src/components/info/login_message.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity } from 'react-native'; 4 | import App from '../../stores/App' 5 | 6 | @observer 7 | export default class LoginMessage extends React.Component{ 8 | 9 | render() { 10 | return( 11 | 12 | App.navigate_to_screen("Login")}> 13 | Please sign in to continue 14 | 15 | 16 | ) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/components/share/dev.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { View, Text } from 'react-native' 4 | import Share from '../../stores/Share' 5 | import App from '../../stores/App' 6 | 7 | @observer 8 | export default class ShareDevComponent extends React.Component { 9 | 10 | render() { 11 | if(!__DEV__) return 12 | return ( 13 | 14 | Users: {Share.users.length} 15 | Is Logged In: {Share.is_logged_in() ? "Yes" : "No"} 16 | 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/sheets/header.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text } from 'react-native'; 4 | import App from '../../stores/App' 5 | 6 | @observer 7 | export default class SheetHeader extends React.Component{ 8 | 9 | render() { 10 | return( 11 | 20 | {this.props.title} 21 | 22 | ) 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /patches/react-native-fs+2.20.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-fs/RNFSManager.m b/node_modules/react-native-fs/RNFSManager.m 2 | index 5ddd941..7bca62a 100755 3 | --- a/node_modules/react-native-fs/RNFSManager.m 4 | +++ b/node_modules/react-native-fs/RNFSManager.m 5 | @@ -294,8 +294,8 @@ + (BOOL)requiresMainQueueSetup 6 | } 7 | 8 | RCT_EXPORT_METHOD(read:(NSString *)filepath 9 | - length: (NSInteger *)length 10 | - position: (NSInteger *)position 11 | + length: (NSInteger)length 12 | + position: (NSInteger)position 13 | resolver:(RCTPromiseResolveBlock)resolve 14 | rejecter:(RCTPromiseRejectBlock)reject) 15 | { 16 | -------------------------------------------------------------------------------- /src/screens/bookmarks/bookmark.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import App from './../../stores/App'; 5 | import GenericScreenComponent from '../../components/generic/generic_screen'; 6 | 7 | @observer 8 | export default class BookmarkScreen extends React.Component{ 9 | 10 | render() { 11 | return ( 12 | 18 | ) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/MBHighlightingTextManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // MBHighlightingTextManager.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 4/16/23. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @class MBHighlightingTextView; 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface MBHighlightingTextManager : RCTViewManager 16 | 17 | @property (strong, nonatomic) id textStorage; 18 | @property (strong, nonatomic) MBHighlightingTextView* textView; 19 | @property (strong, nonatomic) NSTimer* typingTimer; 20 | @property (assign) BOOL isTyping; 21 | 22 | + (CGFloat) preferredTimelineFontSize; 23 | + (CGFloat) preferredPostingFontSize; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /src/components/web/loading_view.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { ActivityIndicator, View, Text } from "react-native" 4 | import App from '../../stores/App' 5 | 6 | @observer 7 | export default class WebLoadingViewModule extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | {this.props.loading_text ?? "Loading posts..."} 14 | 15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/MBHighlightingTextView.h: -------------------------------------------------------------------------------- 1 | // 2 | // MBHighlightingTextView.h 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 4/16/23. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MBHighlightingTextView : UITextView 14 | 15 | @property (copy, nonatomic) RCTBubblingEventBlock onChangeText; 16 | @property (copy, nonatomic) RCTBubblingEventBlock onSelectionChange; 17 | @property (strong, nonatomic) UIView* reactAccessoryView; 18 | @property (assign, nonatomic) CGFloat keyboardHeight; 19 | 20 | - (void) callTextChanged:(NSString *)text; 21 | - (void) callSelectionChanged:(UITextRange *)range; 22 | 23 | - (void) adjustHeight; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /src/components/header/update_reply.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Replies from './../../stores/Replies'; 6 | 7 | @observer 8 | export default class UpdateReplyButton extends React.Component { 9 | 10 | render() { 11 | return ( 12 | { 16 | const sent = await Replies.selected_reply.update_reply() 17 | console.log("Replies:update_reply", sent) 18 | if (sent) { 19 | Keyboard.dismiss() 20 | App.go_back() 21 | } 22 | }} 23 | /> 24 | ) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/components/header/post_button.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | 7 | @observer 8 | export default class PostButton extends React.Component { 9 | 10 | render() { 11 | const { post_status } = Auth.selected_user?.posting 12 | return ( 13 | { 17 | const sent = await Auth.selected_user.posting.send_post() 18 | if (sent) { 19 | Keyboard.dismiss() 20 | App.go_back() 21 | } 22 | }} 23 | /> 24 | ) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/components/header/post_reply.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Reply from '../../stores/Reply'; 6 | 7 | @observer 8 | export default class PostReplyButton extends React.Component { 9 | 10 | render() { 11 | return ( 12 | { 16 | if(this.props.conversation_id != null && Reply.conversation_id != null){ 17 | let sent = await Reply.send_reply() 18 | if (sent) { 19 | Keyboard.dismiss() 20 | App.go_back() 21 | } 22 | } 23 | }} 24 | /> 25 | ) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/screens/discover/topic.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen'; 5 | 6 | @observer 7 | export default class DiscoverTopicScreen extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 17 | ) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/screens/mentions/mentions.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen' 5 | 6 | @observer 7 | export default class MentionsScreen extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 18 | ) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/release/java/blog/micro/android/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package blog.micro.android; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } -------------------------------------------------------------------------------- /src/screens/timeline/timeline.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen'; 5 | import App from '../../stores/App' 6 | 7 | 8 | @observer 9 | export default class TimelineScreen extends React.Component{ 10 | 11 | render() { 12 | return( 13 | <> 14 | 20 | > 21 | ) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/string_utils.js: -------------------------------------------------------------------------------- 1 | String.prototype.InsertTextStyle = function (value_to_insert, selection, is_link = false, url = null) { 2 | const start = selection.start 3 | const end = selection.end + 1 4 | let result = '' 5 | 6 | if (start === end) { 7 | result = `${this.slice(0, start)}${value_to_insert}${this.slice(end - 1)}` 8 | } 9 | else { 10 | const beginning_text = this.slice(0, start) 11 | const selected_text = this.slice(start, end - 1) 12 | const remaining_text = this.slice(end - 1) 13 | 14 | if (is_link) { 15 | const link_text = `[${selected_text}](${url || ''})` 16 | result = `${beginning_text}${link_text}${remaining_text}` 17 | } 18 | else { 19 | result = `${beginning_text}${value_to_insert}${selected_text}${value_to_insert}${remaining_text}` 20 | } 21 | } 22 | 23 | return result; 24 | } -------------------------------------------------------------------------------- /src/components/header/update_page.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from '../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | 7 | @observer 8 | export default class UpdatePageButton extends React.Component { 9 | 10 | render() { 11 | return ( 12 | { 16 | const sent = await Auth.selected_user.posting.send_update_post() 17 | if (sent) { 18 | Auth.selected_user.posting.clear_post() 19 | Keyboard.dismiss() 20 | Auth.selected_user.posting.selected_service.update_pages_for_active_destination() 21 | App.go_back() 22 | } 23 | }} 24 | /> 25 | ) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/utils/dev.js: -------------------------------------------------------------------------------- 1 | import { LogBox } from 'react-native'; 2 | 3 | LogBox.ignoreLogs([ 4 | "Error: Nothing to dismiss", 5 | "Can't perform a React state update on an unmounted component", 6 | "Error: A stack can't contain two children with the same id", 7 | "Cannot record touch end without", 8 | "Require cycle:", 9 | "onAnimatedValueUpdate", 10 | "Cannot update during an existing state transition", 11 | "You are trying to read or write to an object that is no longer part of a state tree", 12 | "Cannot read property 'addNodeToCache' of undefined", 13 | "the creation of the observable instance must be done on the initializing phase", 14 | "`new NativeEventEmitter()` was called with a non-null argument without the required", 15 | "Sending `onAnimatedValueUpdate` with no listeners registered", 16 | "Did not receive response to shouldStartLoad", 17 | "Open debugger to view warnings" 18 | ]) 19 | -------------------------------------------------------------------------------- /src/components/header/screen_title.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, Platform } from 'react-native'; 4 | import ProfileImage from './profile_image'; 5 | import App from '../../stores/App' 6 | 7 | @observer 8 | export default class ScreenTitle extends React.Component{ 9 | 10 | render() { 11 | if (this.props.title) { 12 | return ( 13 | 20 | { Platform.OS === 'android' && } 21 | {this.props.title} 22 | 23 | ) 24 | } 25 | return null 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/color_syntax_tags.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.540", 9 | "green" : "0.150", 10 | "red" : "0.590" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.930", 27 | "green" : "0.670", 28 | "red" : "0.890" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/color_editing_paragraph.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/web/error_view.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { ActivityIndicator, View, Text } from "react-native" 4 | import App from '../../stores/App' 5 | 6 | @observer 7 | export default class WebErrorViewModule extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 12 | Whoops, an error occured. 13 | {this.props.error_name} 14 | Please pull to refresh to try again... 15 | 16 | ) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/screens/following/following.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen' 5 | 6 | @observer 7 | export default class FollowingScreen extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 23 | ) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/components/sheets/login_message.js: -------------------------------------------------------------------------------- 1 | import ActionSheet from "react-native-actions-sheet"; 2 | import * as React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | import { SafeAreaView, Text } from 'react-native'; 5 | import Login from './../../stores/Login'; 6 | 7 | @observer 8 | export default class LoginMessageSheet extends React.Component { 9 | render() { 10 | return ( 11 | 21 | 22 | {Login.message} 23 | 24 | 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/components/header/update_post.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from '../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | 7 | @observer 8 | export default class UpdatePostButton extends React.Component { 9 | 10 | render() { 11 | const { post_status } = Auth.selected_user?.posting; 12 | return ( 13 | { 18 | const sent = await Auth.selected_user.posting.send_update_post() 19 | if (sent) { 20 | Auth.selected_user.posting.clear_post() 21 | Keyboard.dismiss() 22 | Auth.selected_user.posting.selected_service.update_posts_for_active_destination() 23 | App.go_back() 24 | } 25 | }} 26 | /> 27 | ) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | buildToolsVersion = "35.0.0" 5 | minSdkVersion = 24 6 | compileSdkVersion = 35 7 | targetSdkVersion = 35 8 | ndkVersion = "27.1.12297006" 9 | kotlinVersion = "2.0.21" 10 | androidXBrowser = "1.8.0" 11 | } 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | dependencies { 17 | classpath("com.android.tools.build:gradle") 18 | classpath("com.facebook.react:react-native-gradle-plugin") 19 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 20 | classpath 'com.google.gms:google-services:4.3.14' 21 | } 22 | gradle.startParameter.excludedTaskNames.addAll( 23 | gradle.startParameter.taskNames.findAll { it.contains("testClasses") } 24 | ) 25 | } 26 | 27 | apply plugin: "com.facebook.react.rootproject" 28 | apply plugin: "expo-root-project" 29 | -------------------------------------------------------------------------------- /src/screens/conversation/conversation.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from '../../stores/Auth'; 4 | import Reply from '../../stores/Reply' 5 | import GenericScreenComponent from '../../components/generic/generic_screen' 6 | import App from '../../stores/App' 7 | 8 | @observer 9 | export default class ConversationScreen extends React.Component{ 10 | 11 | render() { 12 | return ( 13 | 20 | ) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /android/app/src/main/java/blog/micro/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package blog.micro.android; 2 | import expo.modules.ReactActivityDelegateWrapper 3 | 4 | import com.facebook.react.ReactActivity 5 | import com.facebook.react.ReactActivityDelegate 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate 8 | 9 | 10 | class MainActivity : ReactActivity() { 11 | 12 | 13 | 14 | /** 15 | * Returns the name of the main component registered from JavaScript. This is used to schedule 16 | * rendering of the component. 17 | */ 18 | override fun getMainComponentName(): String = "Micro.blog" 19 | 20 | /** 21 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 22 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 23 | */ 24 | override fun createReactActivityDelegate(): ReactActivityDelegate = 25 | ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)) 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Micro.blog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stores/Settings.js: -------------------------------------------------------------------------------- 1 | import { types, flow, applySnapshot } from 'mobx-state-tree'; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | 4 | export default Settings = types.model('Settings', { 5 | open_links_in_external_browser: types.optional(types.boolean, false), 6 | open_links_with_reader_mode: types.optional(types.boolean, false) 7 | }) 8 | .actions(self => ({ 9 | 10 | hydrate: flow(function* () { 11 | console.log("Settings:hydrate") 12 | const data = yield AsyncStorage.getItem('Settings') 13 | if (data) { 14 | applySnapshot(self, JSON.parse(data)) 15 | console.log("Settings:hydrate:with_data") 16 | } 17 | }), 18 | 19 | toggle_open_links_in_external_browser: flow(function* () { 20 | console.log("Settings:toggle_open_links_in_external_browser") 21 | self.open_links_in_external_browser = !self.open_links_in_external_browser 22 | }), 23 | 24 | toggle_open_links_with_reader_mode: flow(function* () { 25 | console.log("Settings:open_links_with_reader_mode") 26 | self.open_links_with_reader_mode = !self.open_links_with_reader_mode 27 | }) 28 | 29 | })) 30 | .create(); -------------------------------------------------------------------------------- /src/components/generic/loading.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, ActivityIndicator, Text } from 'react-native'; 3 | import App from '../../stores/App'; 4 | 5 | export default class LoadingComponent extends React.Component{ 6 | 7 | render() { 8 | const { should_show, message, size = 'large' } = this.props 9 | if (!should_show) return null 10 | return( 11 | 22 | 27 | 28 | { 29 | message && {message} 30 | } 31 | 32 | 33 | ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/components/sheets/sheets.js: -------------------------------------------------------------------------------- 1 | import { registerSheet } from 'react-native-actions-sheet'; 2 | import LoginMessageSheet from './login_message'; 3 | import SheetMenu from './menu'; 4 | import ProfileMoreMenu from "./profile_more" 5 | import TagmojiMenu from "./tagmoji"; 6 | import PostsDestinationMenu from "./posts_destination"; 7 | import TagsMenu from "./tags"; 8 | import AddTagsMenu from "./add_tags"; 9 | import NotificationsSheetsMenu from "./notifications"; 10 | import UploadInfoSheet from "./upload_info"; 11 | import CollectionsSheet from "./collections"; 12 | import ReplySheet from './reply_sheet'; 13 | 14 | registerSheet("login-message-sheet", LoginMessageSheet) 15 | registerSheet("main_sheet", SheetMenu); 16 | registerSheet("profile_more_menu", ProfileMoreMenu); 17 | registerSheet("tagmoji_menu", TagmojiMenu); 18 | registerSheet("posts_destination_menu", PostsDestinationMenu); 19 | registerSheet("tags_menu", TagsMenu) 20 | registerSheet("add_tags_sheet", AddTagsMenu) 21 | registerSheet("notifications_sheet", NotificationsSheetsMenu) 22 | registerSheet("upload_info_sheet", UploadInfoSheet) 23 | registerSheet("collections_sheet", CollectionsSheet) 24 | registerSheet("reply_sheet", ReplySheet) 25 | 26 | export { } 27 | -------------------------------------------------------------------------------- /patches/react-native-image-viewing+0.2.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js 2 | index 0708505..1b9a7cc 100644 3 | --- a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js 4 | +++ b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js 5 | @@ -26,10 +26,10 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre 6 | const scrollValueY = new Animated.Value(0); 7 | const scaleValue = new Animated.Value(scale || 1); 8 | const translateValue = new Animated.ValueXY(translate); 9 | - const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1; 10 | + const maxScale = scale && scale > 0 ? 3 : 1; 11 | const imageOpacity = scrollValueY.interpolate({ 12 | inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], 13 | - outputRange: [0.5, 1, 0.5], 14 | + outputRange: [0.95, 1, 0.95], 15 | }); 16 | const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue); 17 | const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity }; 18 | -------------------------------------------------------------------------------- /src/components/common/MBImage.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Image as RNImage } from 'react-native' 3 | import { Image as ExpoImage } from 'expo-image' 4 | import App from '../../stores/App' 5 | 6 | const mapContentFitToResizeMode = (contentFit) => { 7 | if (contentFit === 'contain' || contentFit === 'scale-down') { 8 | return 'contain' 9 | } 10 | if (contentFit === 'cover') { 11 | return 'cover' 12 | } 13 | if (contentFit === 'fill') { 14 | return 'stretch' 15 | } 16 | if (contentFit === 'none') { 17 | return 'center' 18 | } 19 | return undefined 20 | } 21 | 22 | const MBImage = React.forwardRef(({ contentFit, ...props }, ref) => { 23 | if (App.is_share_extension) { 24 | const { 25 | transition, 26 | cachePolicy, 27 | placeholder, 28 | ...restProps 29 | } = props 30 | 31 | const { resizeMode, ...nativeProps } = restProps 32 | const finalResizeMode = resizeMode ?? mapContentFitToResizeMode(contentFit) 33 | 34 | return ( 35 | 40 | ) 41 | } 42 | 43 | return ( 44 | 49 | ) 50 | }) 51 | 52 | export default MBImage 53 | -------------------------------------------------------------------------------- /src/screens/bookmarks/bookmarks.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen' 5 | import App from './../../stores/App'; 6 | import TagFilterHeader from '../../components/bookmarks/tag_filter_header'; 7 | 8 | @observer 9 | export default class BookmarksScreen extends React.Component{ 10 | 11 | render() { 12 | return ( 13 | <> 14 | { 15 | Auth.is_logged_in() && Auth.selected_user?.selected_tag != null ? 16 | 17 | : null 18 | } 19 | 27 | > 28 | ) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/components/header/new_collection.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import App from '../../stores/App' 6 | import { SFSymbol } from "react-native-sfsymbols"; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class NewCollectionButton extends React.Component{ 11 | 12 | render() { 13 | return ( 14 | { 21 | App.navigate_to_screen("AddCollection"); 22 | }} 23 | > 24 | { Platform.OS === 'ios' ? 25 | 30 | : 31 | 39 | } 40 | 41 | 42 | ) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/components/header/refresh_activity.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { ActivityIndicator, Platform } from 'react-native'; 4 | import Replies from './../../stores/Replies'; 5 | import Auth from './../../stores/Auth'; 6 | import App from './../../stores/App'; 7 | 8 | @observer 9 | export default class RefreshActivity extends React.Component{ 10 | 11 | render() { 12 | let is_loading = false 13 | switch(this.props.type){ 14 | case "posts": 15 | is_loading = Auth.selected_user.posting.selected_service.is_loading_posts || App.is_searching_posts 16 | break; 17 | case "pages": 18 | is_loading = Auth.selected_user.posting.selected_service.is_loading_pages || App.is_searching_pages 19 | break; 20 | case "uploads": 21 | is_loading = Auth.selected_user.posting.selected_service.is_loading_uploads 22 | break; 23 | case "highlights": 24 | is_loading = App.is_loading_highlights 25 | break; 26 | default: 27 | is_loading = Replies.is_loading 28 | } 29 | return( 30 | 31 | ) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/screens/discover/discover.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import TagmojiBar from '../../components/discover/tagmoji_bar' 5 | import Discover from '../../stores/Discover' 6 | import GenericScreenComponent from '../../components/generic/generic_screen'; 7 | 8 | @observer 9 | export default class DiscoverScreen extends React.Component{ 10 | 11 | componentDidMount() { 12 | Discover.init() 13 | } 14 | 15 | render() { 16 | return ( 17 | <> 18 | { 19 | Auth.is_logged_in() ? 20 | 21 | : null 22 | } 23 | 30 | > 31 | ) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/components/generic/generic_screen.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View } from 'react-native'; 4 | import LoginMessage from '../info/login_message'; 5 | import ImageModalModule from '../images/image_modal'; 6 | import WebViewModule from '../web/webview_module'; 7 | import App from '../../stores/App'; 8 | 9 | @observer 10 | export default class GenericScreenComponent extends React.Component{ 11 | 12 | render() { 13 | return( 14 | 15 | { 16 | this.props.can_show_web_view != null && this.props.can_show_web_view ? 17 | <> 18 | {this.props.children} 19 | 20 | > 21 | : 22 | !this.props.is_search && !this.props.is_filtered && !App.is_changing_font_scale && !App.is_loading_bookmarks && 23 | } 24 | 25 | 26 | ) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/snapshots.js: -------------------------------------------------------------------------------- 1 | import { onSnapshot } from 'mobx-state-tree'; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | import * as SecureStore from 'expo-secure-store'; 4 | import SFInfo from 'react-native-sensitive-info' 5 | 6 | import Tokens from './../stores/Tokens'; 7 | import Auth from './../stores/Auth'; 8 | import Settings from './../stores/Settings'; 9 | import Share from '../stores/Share' 10 | 11 | function debounce(func, wait) { 12 | let timeout 13 | return function (...args) { 14 | const context = this 15 | clearTimeout(timeout) 16 | timeout = setTimeout(() => func.apply(context, args), wait) 17 | } 18 | } 19 | 20 | const debounce_ms = 1500 21 | 22 | onSnapshot(Tokens, snapshot => { 23 | SFInfo.setItem('Tokens', JSON.stringify(snapshot), {}), 24 | SecureStore.setItem('Tokens', JSON.stringify(snapshot), {}), 25 | console.log("SNAPSHOT:::TOKENS") 26 | }); 27 | onSnapshot(Auth, debounce( 28 | snapshot => { 29 | AsyncStorage.setItem('Auth', JSON.stringify(snapshot)); 30 | console.log("SNAPSHOT:::AUTH") 31 | }, debounce_ms )) 32 | onSnapshot(Settings, snapshot => { AsyncStorage.setItem('Settings', JSON.stringify(snapshot)), console.log("SNAPSHOT:::SETTINGS") }) 33 | onSnapshot(Share, snapshot => { AsyncStorage.setItem('Share', JSON.stringify(snapshot)), console.log("SNAPSHOT:::SHARE") }); 34 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") 2 | def expoPluginsPath = new File( 3 | providers.exec { 4 | workingDir(rootDir) 5 | commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") 6 | }.standardOutput.asText.get().trim(), 7 | "../android/expo-gradle-plugin" 8 | ).absolutePath 9 | includeBuild(expoPluginsPath) 10 | } 11 | plugins { id("com.facebook.react.settings") 12 | id("expo-autolinking-settings") 13 | } 14 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> 15 | def command = [ 16 | 'node', 17 | '--no-warnings', 18 | '--eval', 19 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 20 | 'react-native-config', 21 | '--json', 22 | '--platform', 23 | 'android' 24 | ].toList() 25 | ex.autolinkLibrariesFromCommand(command) 26 | } 27 | rootProject.name = 'Micro.blog' 28 | include ':app' 29 | includeBuild('../node_modules/@react-native/gradle-plugin') 30 | 31 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") 32 | useExpoModules() 33 | expoAutolinking.useExpoVersionCatalog() 34 | includeBuild(expoAutolinking.reactNativeGradlePlugin) 35 | -------------------------------------------------------------------------------- /src/components/cells/checkmark_row_cell.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { View, Text, Platform } from 'react-native' 3 | import App from '../../stores/App' 4 | import { SvgXml } from 'react-native-svg'; 5 | import { SFSymbol } from "react-native-sfsymbols"; 6 | 7 | export default class CheckmarkRowCell extends React.Component { 8 | 9 | constructor (props) { 10 | super(props); 11 | this.state = { 12 | text: props.text, 13 | is_selected: props.is_selected 14 | }; 15 | } 16 | 17 | render() { 18 | const { text, is_selected } = this.props; 19 | return ( 20 | 21 | {text} 22 | { 23 | is_selected && Platform.OS === 'ios' ? 24 | 25 | : is_selected ? 26 | 33 | : null 34 | } 35 | 36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ios/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryFileTimestamp 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | 0A2A.1 21 | 3B52.1 22 | C617.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategoryDiskSpace 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | E174.1 31 | 85F4.1 32 | 33 | 34 | 35 | NSPrivacyAccessedAPIType 36 | NSPrivacyAccessedAPICategorySystemBootTime 37 | NSPrivacyAccessedAPITypeReasons 38 | 39 | 35F9.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/header/remove_image.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Alert } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | import { StackActions } from '@react-navigation/native'; 7 | 8 | @observer 9 | export default class RemoveImageButton extends React.Component { 10 | 11 | _handle_image_remove = () => { 12 | const { posting } = Auth.selected_user 13 | const { asset, index } = this.props; 14 | const existing_index = posting.post_assets?.findIndex(file => file.uri === asset.uri) 15 | if (existing_index > -1) { 16 | Alert.alert( 17 | "Remove upload?", 18 | "Are you sure you want to remove this upload from this post?", 19 | [ 20 | { 21 | text: "Cancel", 22 | style: 'cancel', 23 | }, 24 | { 25 | text: "Remove", 26 | onPress: () => { 27 | this.props.navigation.goBack() 28 | // delay, seems to create problems otherwise 29 | setTimeout(() => { 30 | posting.remove_asset(index) 31 | }, 500); 32 | }, 33 | style: 'destructive' 34 | }, 35 | ], 36 | {cancelable: false}, 37 | ); 38 | } 39 | } 40 | 41 | render() { 42 | return ( 43 | 48 | ) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/components/header/add_bookmark.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import App from '../../stores/App' 6 | import { SFSymbol } from "react-native-sfsymbols"; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class AddBookmarkButton extends React.Component{ 11 | 12 | render() { 13 | if(Auth.selected_user != null && Auth.selected_user.posting?.posting_enabled()){ 14 | return( 15 | App.navigate_to_screen("add_bookmark")} 22 | > 23 | { 24 | Platform.OS === 'ios' ? 25 | 30 | : 31 | 39 | } 40 | 41 | 42 | ) 43 | } 44 | return null 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/components/header/new_post.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Image, Platform } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import PostAddIcon from './../../assets/icons/post_add.png'; 6 | import App from '../../stores/App' 7 | import { isLiquidGlass } from './../../utils/ui'; 8 | import { SFSymbol } from "react-native-sfsymbols"; 9 | 10 | @observer 11 | export default class NewPostButton extends React.Component{ 12 | 13 | render() { 14 | let button_style = { 15 | justifyContent: 'center', 16 | alignItems: 'center' 17 | }; 18 | 19 | if (isLiquidGlass()) { 20 | button_style.paddingLeft = 4; 21 | } 22 | 23 | if(Auth.selected_user != null && Auth.selected_user.posting?.posting_enabled()){ 24 | return ( 25 | App.navigate_to_screen("Posting")} 28 | accessibilityRole="button" 29 | accessibilityLabel="New post" 30 | > 31 | { 32 | Platform.OS === 'ios' ? 33 | 38 | : 39 | 40 | } 41 | 42 | 43 | ) 44 | } 45 | return null 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/components/header/close.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class CloseModalButton extends React.Component { 11 | 12 | render() { 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style.paddingLeft = 7; 17 | button_style.paddingTop = 4; 18 | } 19 | 20 | return ( 21 | App.go_back()} 23 | style={button_style} 24 | > 25 | { 26 | Platform.OS === 'ios' ? 27 | 32 | : 33 | 45 | } 46 | 47 | ) 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /patches/react-native-sensitive-info@5.5.8.patch: -------------------------------------------------------------------------------- 1 | diff --git a/RNSensitiveInfo.js b/RNSensitiveInfo.js 2 | index b3e164c6bb00e1bb885e796f6f402909ecfb6071..34cec63f2020108cac0051861175a08c92aa5817 100644 3 | --- a/RNSensitiveInfo.js 4 | +++ b/RNSensitiveInfo.js 5 | @@ -1,23 +1,23 @@ 6 | -import {NativeModules} from 'react-native'; 7 | +import { NativeModules } from 'react-native'; 8 | 9 | const RNSensitiveInfo = NativeModules.RNSensitiveInfo; 10 | 11 | -module.exports = { 12 | - ...RNSensitiveInfo, 13 | - setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment) { 14 | - if (RNSensitiveInfo.setInvalidatedByBiometricEnrollment == null) { 15 | - return; 16 | - } 17 | +RNSensitiveInfo.setInvalidatedByBiometricEnrollment = ( 18 | + invalidatedByBiometricEnrollment, 19 | +) => { 20 | + if (RNSensitiveInfo.setInvalidatedByBiometricEnrollment == null) { 21 | + return undefined; 22 | + } 23 | 24 | - return RNSensitiveInfo.setInvalidatedByBiometricEnrollment( 25 | - invalidatedByBiometricEnrollment, 26 | - ); 27 | - }, 28 | - cancelFingerprintAuth() { 29 | - if (RNSensitiveInfo.cancelFingerprintAuth == null) { 30 | - return; 31 | - } 32 | + return RNSensitiveInfo.setInvalidatedByBiometricEnrollment( 33 | + invalidatedByBiometricEnrollment, 34 | + ); 35 | +}; 36 | +RNSensitiveInfo.cancelFingerprintAuth = () => { 37 | + if (RNSensitiveInfo.cancelFingerprintAuth == null) { 38 | + return undefined; 39 | + } 40 | 41 | - return RNSensitiveInfo.cancelFingerprintAuth(); 42 | - }, 43 | + return RNSensitiveInfo.cancelFingerprintAuth(); 44 | }; 45 | +export default RNSensitiveInfo; 46 | -------------------------------------------------------------------------------- /src/screens/profile/profile.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import WebViewModule from '../../components/web/webview_module' 6 | import LoginMessage from '../../components/info/login_message' 7 | import ImageModalModule from '../../components/images/image_modal' 8 | import ProfileHeader from '../../components/profile/profile_header' 9 | import MutedMessage from '../../components/info/muted_message' 10 | import App from '../../stores/App' 11 | 12 | @observer 13 | export default class ProfileScreen extends React.Component{ 14 | 15 | render() { 16 | const { username } = this.props.route.params 17 | const is_muted = Auth.selected_user?.muting?.is_muted(username) 18 | const is_blocked = Auth.selected_user?.muting?.blocked_users.some(u => u.username === username) 19 | 20 | return( 21 | 22 | { 23 | Auth.is_logged_in() && !Auth.is_selecting_user && !App.should_reload_web_view() ? 24 | is_muted || is_blocked ? 25 | 26 | : 27 | : null} /> 28 | : 29 | 30 | } 31 | 32 | 33 | ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/components/header/close_post_clear.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class ClosePostClearButton extends React.Component { 11 | 12 | render() { 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style.paddingLeft = 8; 17 | button_style.paddingTop = 5; 18 | } 19 | 20 | return ( 21 | { 23 | App.go_back_and_clear() 24 | }} 25 | style={button_style} 26 | > 27 | { 28 | Platform.OS === 'ios' ? 29 | 34 | : 35 | 47 | } 48 | 49 | ) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/components/header/reply.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class ReplyButton extends React.Component { 11 | 12 | render() { 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style = { 17 | marginLeft: 7, 18 | marginTop: 2 19 | } 20 | } 21 | 22 | return ( 23 | { 25 | if(this.props.conversation_id != null){ 26 | App.open_sheet("reply_sheet", { conversation_id: this.props.conversation_id }) 27 | } 28 | }} 29 | style={button_style} 30 | > 31 | { 32 | Platform.OS === 'ios' ? 33 | 38 | : 39 | 47 | } 48 | 49 | ) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/components/images/image_modal.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import ImageView from "react-native-image-viewing"; 4 | import App from "../../stores/App" 5 | import { Platform, SafeAreaView, TouchableOpacity, Image } from 'react-native' 6 | import { SFSymbol } from "react-native-sfsymbols"; 7 | import ArrowBackIcon from './../../assets/icons/arrow_back.png'; 8 | 9 | @observer 10 | export default class ImageModalModule extends React.Component{ 11 | 12 | close_button = () => { 13 | return ( 14 | 15 | 16 | { 17 | Platform.OS === 'ios' ? 18 | 28 | : 29 | 34 | } 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | render() { 42 | if (App.image_modal_is_open) { 43 | return ( 44 | this.close_button()} 50 | doubleTapToZoomEnabled={true} 51 | /> 52 | ) 53 | } 54 | return null 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/screens/stacks/MentionsStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import MentionsScreen from '../mentions/mentions'; 6 | import ProfileImage from './../../components/header/profile_image'; 7 | import NewPostButton from '../../components/header/new_post'; 8 | import BackButton from '../../components/header/back'; 9 | import { getSharedScreens } from './SharedStack' 10 | import App from '../../stores/App' 11 | 12 | const MentionsStack = createNativeStackNavigator(); 13 | 14 | @observer 15 | export default class Mentions extends React.Component{ 16 | 17 | render() { 18 | const sharedScreens = getSharedScreens(MentionsStack, "Mentions") 19 | return( 20 | 27 | ({ 31 | headerLeft: () => , 32 | headerRight: () => 33 | })} 34 | /> 35 | ({ 37 | headerLeft: () => , 38 | headerBackTitleVisible: false 39 | })} 40 | > 41 | {sharedScreens} 42 | 43 | 44 | ) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/screens/stacks/TimelineStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { observer } from 'mobx-react'; 4 | import { Platform } from 'react-native'; 5 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 6 | import TimelineScreen from '../timeline/timeline'; 7 | import ProfileImage from './../../components/header/profile_image'; 8 | import NewPostButton from '../../components/header/new_post'; 9 | import BackButton from '../../components/header/back'; 10 | import { getSharedScreens } from './SharedStack' 11 | import App from '../../stores/App' 12 | 13 | const TimelineStack = createNativeStackNavigator(); 14 | 15 | @observer 16 | export default class Timeline extends React.Component{ 17 | 18 | render() { 19 | const sharedScreens = getSharedScreens(TimelineStack, "Timeline") 20 | return( 21 | 28 | ({ 32 | headerLeft: () => , 33 | headerRight: () => , 34 | })} 35 | /> 36 | ({ 38 | headerLeft: () => , 39 | headerBackTitleVisible: false, 40 | })} 41 | > 42 | {sharedScreens} 43 | 44 | 45 | ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | .kotlin/ 37 | 38 | # node.js 39 | # 40 | node_modules/ 41 | npm-debug.log 42 | yarn-error.log 43 | 44 | # BUCK 45 | buck-out/ 46 | \.buckd/ 47 | *.keystore 48 | !debug.keystore 49 | 50 | # fastlane 51 | # 52 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 53 | # screenshots whenever they are needed. 54 | # For more information about the recommended setup visit: 55 | # https://docs.fastlane.tools/best-practices/source-control/ 56 | 57 | */fastlane/report.xml 58 | */fastlane/Preview.html 59 | */fastlane/screenshots 60 | **/fastlane/test_output 61 | 62 | # Bundle artifact 63 | *.jsbundle 64 | 65 | # Ruby / CocoaPods 66 | # CocoaPods 67 | **/Pods/ 68 | /vendor/bundle/ 69 | 70 | # Temporary files created by Metro to check the health of the file watcher 71 | .metro-health-check* 72 | 73 | # Yarn 74 | .yarn/* 75 | !.yarn/patches 76 | !.yarn/plugins 77 | !.yarn/releases 78 | !.yarn/sdks 79 | !.yarn/versions 80 | 81 | # User generated 82 | .nova 83 | /android/app/release 84 | /android/app/google-services.json 85 | vendor 86 | ios/.xcode.env.local 87 | /vendor/bundle/ 88 | .yarn/install-state.gz 89 | CLAUDE.md 90 | .cursor 91 | # Expo 92 | .expo 93 | dist/ 94 | web-build/ 95 | -------------------------------------------------------------------------------- /src/screens/stacks/PostEditStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import PostEditScreen from '../posts/edit'; 6 | import PostingOptionsScreen from '../../screens/posts/options'; 7 | import ClosePostClearButton from '../../components/header/close_post_clear'; 8 | import UpdatePostButton from '../../components/header/update_post'; 9 | import App from '../../stores/App' 10 | 11 | const PostingEditStack = createNativeStackNavigator(); 12 | 13 | @observer 14 | export default class PostEditStack extends React.Component{ 15 | 16 | render() { 17 | return( 18 | 29 | , 35 | headerLeft: () => 36 | }} 37 | /> 38 | 45 | 46 | ) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/stores/Reporting.js: -------------------------------------------------------------------------------- 1 | import { types, flow } from 'mobx-state-tree'; 2 | import { Alert, ToastAndroid } from 'react-native'; 3 | import MicroBlogApi, { API_ERROR, REPORTING_ERROR } from '../api/MicroBlogApi' 4 | import Toast from 'react-native-simple-toast'; 5 | 6 | export default Reporting = types.model('Reporting', { 7 | is_sending_report: types.optional(types.boolean, false) 8 | }) 9 | .actions(self => ({ 10 | 11 | init: flow(function* () { 12 | console.log("Reporting:init") 13 | const muted_users = yield MicroBlogApi.get_muted_users(); 14 | if (muted_users && muted_users !== API_ERROR) { 15 | self.muted_users = muted_users; 16 | } 17 | }), 18 | 19 | report_user: flow(function* (username) { 20 | console.log("Reporting:report_user", username) 21 | Alert.alert( 22 | `Report @${username}?`, 23 | `Report @${username} to Micro.blog for review? We'll look at this user's posts to determine if they violate our community guidelines.`, 24 | [ 25 | { 26 | text: "Cancel", 27 | style: 'cancel', 28 | }, 29 | { 30 | text: "Report", 31 | onPress: () => Reporting.handle_report_user(username), 32 | style: 'destructive' 33 | }, 34 | ], 35 | {cancelable: false}, 36 | ); 37 | }), 38 | 39 | handle_report_user: flow(function* (username) { 40 | console.log("Reporting:handle_report_user", username) 41 | self.is_sending_report = true; 42 | const report = yield MicroBlogApi.report_user(username) 43 | if (report !== REPORTING_ERROR) { 44 | Toast.showWithGravity(`@${ username } has been reported.`, Toast.SHORT, Toast.CENTER) 45 | } 46 | else { 47 | alert("Something went wrong. Please try again.") 48 | } 49 | self.is_sending_report = false; 50 | }) 51 | 52 | })) 53 | .views(self => ({ 54 | 55 | })) 56 | .create() 57 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/UIBarButtonItem+Plainify.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+Plainify.m 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 8/30/25. 6 | // 7 | 8 | #import "UIBarButtonItem+Plainify.h" 9 | 10 | @import ObjectiveC; 11 | 12 | @implementation UIBarButtonItem (Plainify) 13 | 14 | + (void) load 15 | { 16 | static dispatch_once_t once_token; 17 | dispatch_once(&once_token, ^{ 18 | Class cls = self; 19 | 20 | Method origInit = class_getInstanceMethod(cls, @selector(initWithCustomView:)); 21 | Method swizInit = class_getInstanceMethod(cls, @selector(mb_initWithCustomView:)); 22 | if (origInit && swizInit) { 23 | method_exchangeImplementations(origInit, swizInit); 24 | } 25 | }); 26 | } 27 | 28 | - (instancetype) mb_initWithCustomView:(UIView *)customView 29 | { 30 | // call original (now-swizzled) init 31 | UIBarButtonItem *item = [self mb_initWithCustomView:customView]; 32 | 33 | if (@available(iOS 26, *)) { 34 | UIActivityIndicatorView* spinner = [self findFirstIndicatorInView:customView]; 35 | if (spinner) { 36 | [item performSelector:@selector(setHidesSharedBackground:) withObject:@(YES)]; 37 | } 38 | } 39 | 40 | return item; 41 | } 42 | 43 | - (UIActivityIndicatorView *) findFirstIndicatorInView:(UIView *)rootView 44 | { 45 | NSMutableArray* queue = [NSMutableArray arrayWithObject:rootView]; 46 | 47 | while (queue.count > 0) { 48 | UIView* view = [queue firstObject]; 49 | [queue removeObjectAtIndex:0]; 50 | 51 | if ([view isKindOfClass:[UIActivityIndicatorView class]]) { 52 | return (UIActivityIndicatorView *)view; 53 | } 54 | 55 | // add subviews to the queue 56 | for (UIView *subview in view.subviews) { 57 | [queue addObject:subview]; 58 | } 59 | } 60 | 61 | return nil; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /ios/MicroBlog_Share/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HostAppBundleIdentifier 6 | blog.micro.ios 7 | HostAppURLScheme 8 | microblog:// 9 | NSExtension 10 | 11 | NSAppTransportSecurity 12 | 13 | NSAllowsArbitraryLoads 14 | 15 | NSExceptionDomains 16 | 17 | localhost 18 | 19 | NSExceptionAllowsInsecureHTTPLoads 20 | 21 | 22 | 23 | 24 | NSExtensionAttributes 25 | 26 | NSExtensionActivationRule 27 | 28 | NSExtensionActivationSupportsImageWithMaxCount 29 | 1 30 | NSExtensionActivationSupportsText 31 | 32 | NSExtensionActivationSupportsWebPageWithMaxCount 33 | 1 34 | NSExtensionActivationSupportsWebURLWithMaxCount 35 | 1 36 | 37 | NSExtensionJavaScriptPreprocessingFile 38 | GetURL 39 | 40 | NSExtensionMainStoryboard 41 | MainInterface 42 | NSExtensionPointIdentifier 43 | com.apple.share-services 44 | ReactShareViewBackgroundColor 45 | 46 | Alpha 47 | 1 48 | Blue 49 | 1 50 | Green 51 | 1 52 | Red 53 | 1 54 | Transparent 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/stores/models/posting/Page.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | import { DOMParser } from "@xmldom/xmldom"; 3 | 4 | let html_parser = new DOMParser({ onError: (error) => { 5 | // silently ignore errors 6 | }}); 7 | 8 | export default Page = types.model('Page', { 9 | uid: types.identifierNumber, 10 | name: types.maybe(types.string), 11 | content: types.maybe(types.string), 12 | published: types.maybe(types.string), 13 | url: types.maybe(types.string), 14 | template: types.optional(types.boolean, false) 15 | }) 16 | .views(self => ({ 17 | 18 | plain_text_content(){ 19 | let html = "" + self.content + ""; 20 | var text = ""; 21 | try { 22 | let doc = html_parser.parseFromString(html, "text/html"); 23 | text = doc.documentElement.textContent; 24 | } 25 | catch (error) { 26 | // if parse error, just show HTML 27 | text = self.content; 28 | } 29 | 30 | if (text.length > 300) { 31 | text = text.substring(0, 300) + '...' 32 | } 33 | 34 | return text.trim(); 35 | }, 36 | 37 | images_from_content(){ 38 | if (!self.content) { 39 | return [] 40 | } 41 | 42 | const img_regex = //g 43 | const video_regex = //g 44 | const images = [] 45 | 46 | let match 47 | while ((match = img_regex.exec(self.content)) !== null) { 48 | images.push(match[1]) 49 | } 50 | 51 | while ((match = video_regex.exec(self.content)) !== null) { 52 | const poster = match[1] ? match[1].trim() : "" 53 | if (poster.length > 0) { 54 | images.push(poster) 55 | } 56 | } 57 | 58 | return images; 59 | }, 60 | 61 | nice_local_published_date(){ 62 | const date = new Date(self.published); 63 | return date.toLocaleString(); 64 | }, 65 | 66 | })) 67 | -------------------------------------------------------------------------------- /src/components/tabs/tab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform, Image } from 'react-native'; 4 | import { SFSymbol } from "react-native-sfsymbols"; 5 | import App from '../../stores/App'; 6 | import TimelineIcon from './../../assets/icons/tab_bar/timeline.png'; 7 | import MentionsIcon from './../../assets/icons/tab_bar/mentions.png'; 8 | import DiscoverIcon from './../../assets/icons/tab_bar/discover.png'; 9 | import BookmarksIcon from './../../assets/icons/nav/bookmarks.png'; 10 | 11 | @observer 12 | export default class Tab extends React.Component { 13 | 14 | _returnIconNameOrAsset() { 15 | const { route } = this.props; 16 | const isIOS = Platform.OS === "ios"; 17 | 18 | switch (route.name) { 19 | case "TimelineStack": 20 | return isIOS ? "bubble.left.and.bubble.right" : TimelineIcon; 21 | case "MentionsStack": 22 | return isIOS ? "at" : MentionsIcon; 23 | case "DiscoverStack": 24 | return isIOS ? "magnifyingglass" : DiscoverIcon; 25 | case "BookmarksStack": 26 | return isIOS ? "star" : BookmarksIcon; 27 | default: 28 | return isIOS ? "questionmark" : null; 29 | } 30 | } 31 | 32 | render() { 33 | const iconNameOrAsset = this._returnIconNameOrAsset(); 34 | const { focused } = this.props; 35 | const color = focused ? App.theme_accent_color() : App.theme_text_color(); 36 | 37 | if (Platform.OS === "ios") { 38 | return ( 39 | 45 | ); 46 | } else { 47 | return iconNameOrAsset ? ( 48 | 52 | ) : null; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/header/profile_image.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, View, Image } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import App from '../../stores/App'; 6 | import { isLiquidGlass } from './../../utils/ui'; 7 | 8 | @observer 9 | export default class ProfileImage extends React.Component{ 10 | 11 | render() { 12 | const routeKey = this.props.routeKey || 'default' 13 | let button_style = { 14 | width: 28, 15 | height: 28, 16 | marginRight: 12 17 | }; 18 | 19 | if (isLiquidGlass()) { 20 | button_style = { 21 | paddingLeft: 4, 22 | paddingTop: 2 23 | } 24 | } 25 | 26 | if(Auth.selected_user != null){ 27 | return( 28 | { App.open_sheet("main_sheet"); Auth.selected_user.check_token_validity()} } 31 | onLongPress={() => App.navigate_to_screen("user", Auth.selected_user.username)} 32 | accessibilityRole="button" 33 | accessibilityLabel={`Profile menu for ${Auth.selected_user.username}`} 34 | > 35 | { 36 | Auth.selected_user.avatar != null && Auth.selected_user.avatar !== "" ? 37 | 45 | : 46 | 47 | } 48 | 49 | 50 | ) 51 | } 52 | return 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/components/sheets/notifications.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; 4 | import App from '../../stores/App'; 5 | import PushNotifications from "../push/push_notifications"; 6 | import ActionSheet from "react-native-actions-sheet"; 7 | import Push from '../../stores/Push'; 8 | 9 | @observer 10 | export default class NotificationsSheetMenu extends React.Component{ 11 | 12 | render() { 13 | return( 14 | Push.toggle_notifications_open(true)} 23 | onClose={() => Push.toggle_notifications_open(false)} 24 | containerStyle={{ 25 | backgroundColor: App.theme_background_color_secondary(), 26 | padding: 15, 27 | borderRadius: 16 28 | }} 29 | > 30 | 31 | Notifications 32 | 33 | Clear all 34 | 35 | 36 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/components/sheets/menu.js: -------------------------------------------------------------------------------- 1 | import ActionSheet from "react-native-actions-sheet"; 2 | import * as React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | import { View, Text, TouchableOpacity, Platform } from 'react-native'; 5 | import Auth from './../../stores/Auth'; 6 | import AccountSwitcher from '../menu/account_switcher' 7 | import MenuNavigation from '../menu/nav' 8 | import App from '../../stores/App'; 9 | 10 | @observer 11 | export default class SheetMenu extends React.Component{ 12 | 13 | render() { 14 | return( 15 | 26 | 35 | 36 | { 37 | Auth.selected_user != null ? 38 | 39 | : 40 | 49 | Please sign in to continue 50 | 51 | } 52 | 53 | 54 | 55 | ) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /ios/MicroBlog_Share/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/stores/Replies.js: -------------------------------------------------------------------------------- 1 | import { types, flow, destroy } from 'mobx-state-tree'; 2 | import MicroBlogApi, { API_ERROR, DELETE_ERROR } from '../api/MicroBlogApi'; 3 | import Reply from './models/Reply' 4 | import App from './App' 5 | import { Alert } from 'react-native'; 6 | import Auth from './Auth'; 7 | 8 | export default Replies = types.model('Replies', { 9 | current_username: types.maybeNull(types.string), 10 | replies: types.optional(types.array(Reply), []), 11 | is_loading: types.optional(types.boolean, false), 12 | selected_reply: types.maybeNull(types.reference(Reply)) 13 | }) 14 | .actions(self => ({ 15 | 16 | hydrate: flow(function* () { 17 | console.log("Replies:hydrate") 18 | if(self.current_username == null || self.current_username !== Auth.selected_user?.username){ 19 | self.replies = [] 20 | self.current_username = Auth.selected_user?.username 21 | } 22 | self.selected_reply = null 23 | self.is_loading = true 24 | const replies = yield MicroBlogApi.get_replies() 25 | if(replies !== API_ERROR && replies.items != null){ 26 | self.replies = replies.items 27 | } 28 | self.is_loading = false 29 | }), 30 | 31 | refresh: flow(function* () { 32 | self.hydrate() 33 | }), 34 | 35 | select_reply_and_open_edit: flow(function* (reply) { 36 | console.log("Reply:select_reply", reply) 37 | self.selected_reply = reply 38 | App.navigate_to_screen("reply_edit") 39 | }), 40 | 41 | delete_reply: flow(function* (reply) { 42 | console.log("Reply:delete_reply", reply) 43 | const reply_id = reply.id 44 | destroy(reply) 45 | const status = yield MicroBlogApi.delete_post(reply_id) 46 | if(status !== DELETE_ERROR){ 47 | App.show_toast("Reply was deleted.") 48 | self.hydrate() 49 | } 50 | else{ 51 | Alert.alert("Whoops", "Could not delete reply. Please try again.") 52 | self.hydrate() 53 | self.is_loading = false 54 | } 55 | }), 56 | 57 | })) 58 | .create({}) -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Use this property to specify which architecture you want to build. 28 | # You can also override it from the CLI using 29 | # ./gradlew -PreactNativeArchitectures=x86_64 30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 31 | 32 | # Use this property to enable support to the new architecture. 33 | # This will allow you to use TurboModules and the Fabric render in 34 | # your application. You should enable this flag either if you want 35 | # to write custom TurboModules/Fabric components OR use libraries that 36 | # are providing them. 37 | newArchEnabled=true 38 | 39 | # Use this property to enable or disable the Hermes JS engine. 40 | # If set to false, you will be using JSC instead. 41 | hermesEnabled=true 42 | -------------------------------------------------------------------------------- /src/components/header/new_upload.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Image, Platform } from 'react-native' 4 | import Auth from './../../stores/Auth' 5 | import App from '../../stores/App' 6 | import { SFSymbol } from "react-native-sfsymbols" 7 | import { MenuView } from '@react-native-menu/menu'; 8 | import AddIcon from './../../assets/icons/add.png' 9 | 10 | @observer 11 | export default class NewUploadButton extends React.Component { 12 | 13 | render() { 14 | if (Auth.selected_user != null && Auth.selected_user.posting?.posting_enabled()) { 15 | const { config } = Auth.selected_user.posting.selected_service 16 | return ( 17 | { 19 | const event_id = nativeEvent.event 20 | if (event_id === 'upload_media') { 21 | console.log('upload_media') 22 | Auth.selected_user.posting.selected_service?.pick_image(config?.temporary_destination()) 23 | } else if (event_id === 'upload_file') { 24 | console.log('upload_file') 25 | Auth.selected_user.posting.selected_service?.pick_file(config?.temporary_destination()) 26 | } 27 | }} 28 | actions={[ 29 | { 30 | title: "Photo library", 31 | id: "upload_media", 32 | image: Platform.select({ 33 | ios: 'photo' 34 | }) 35 | }, 36 | { 37 | title: "Files", 38 | id: "upload_file", 39 | image: Platform.select({ 40 | ios: 'folder' 41 | }) 42 | } 43 | ]} 44 | > 45 | { 46 | Platform.OS === 'ios' ? 47 | 52 | : 53 | 57 | } 58 | 59 | ) 60 | } 61 | return null 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/components/search_bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Text, View, TextInput, TouchableOpacity } from 'react-native'; 4 | import App from '../stores/App'; 5 | 6 | @observer 7 | export default class SearchBar extends React.Component { 8 | render() { 9 | const { 10 | placeholder, 11 | onSubmitEditing, 12 | onChangeText, 13 | value, 14 | onCancel 15 | } = this.props; 16 | 17 | return ( 18 | 28 | 54 | 64 | 69 | Cancel 70 | 71 | 72 | 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/stores/models/posting/Post.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | import { DOMParser } from "@xmldom/xmldom"; 3 | 4 | let html_parser = new DOMParser({ onError: (error) => { 5 | // silently ignore errors 6 | }}); 7 | 8 | export default Post = types.model('Post', { 9 | uid: types.identifierNumber, 10 | name: types.maybe(types.string), 11 | content: types.maybe(types.string), 12 | published: types.maybe(types.string), 13 | url: types.maybe(types.string), 14 | post_status: types.maybe(types.string), 15 | category: types.optional(types.array(types.string), []), 16 | summary: types.maybeNull(types.string) 17 | }) 18 | .views(self => ({ 19 | 20 | plain_text_content(){ 21 | let html = "" + self.content + ""; 22 | var text = ""; 23 | try { 24 | let doc = html_parser.parseFromString(html, "text/html"); 25 | text = doc.documentElement.textContent; 26 | } 27 | catch (error) { 28 | // if parse error, just show HTML 29 | text = self.content; 30 | } 31 | 32 | if (text.length > 300) { 33 | text = text.substring(0, 300) + '...' 34 | } 35 | 36 | return text.trim(); 37 | }, 38 | 39 | images_from_content(){ 40 | if (!self.content) { 41 | return [] 42 | } 43 | 44 | const img_regex = //g 45 | const video_regex = //g 46 | const images = [] 47 | 48 | let match 49 | while ((match = img_regex.exec(self.content)) !== null) { 50 | images.push(match[1]) 51 | } 52 | 53 | while ((match = video_regex.exec(self.content)) !== null) { 54 | const poster = match[1] ? match[1].trim() : "" 55 | if (poster.length > 0) { 56 | images.push(poster) 57 | } 58 | } 59 | 60 | return images; 61 | }, 62 | 63 | nice_local_published_date(){ 64 | const date = new Date(self.published); 65 | return date.toLocaleString(); 66 | }, 67 | 68 | is_draft() { 69 | return self.post_status == "draft" 70 | } 71 | 72 | })) 73 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | # Resolve react_native_pods.rb with node to allow for hoisting 3 | require Pod::Executable.execute_command('node', ['-p', 4 | 'require.resolve( 5 | "react-native/scripts/react_native_pods.rb", 6 | {paths: [process.argv[1]]}, 7 | )', __dir__]).strip 8 | 9 | platform :ios, min_ios_version_supported 10 | prepare_react_native_project! 11 | 12 | linkage = ENV['USE_FRAMEWORKS'] 13 | if linkage != nil 14 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 15 | use_frameworks! :linkage => linkage.to_sym 16 | end 17 | 18 | target 'MicroBlog_RN' do 19 | use_expo_modules! 20 | 21 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' 22 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; 23 | else 24 | config_command = [ 25 | 'node', 26 | '--no-warnings', 27 | '--eval', 28 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 29 | 'react-native-config', 30 | '--json', 31 | '--platform', 32 | 'ios' 33 | ] 34 | end 35 | 36 | config = use_native_modules!(config_command) 37 | 38 | use_react_native!( 39 | :path => config[:reactNativePath], 40 | # An absolute path to your application root. 41 | :app_path => "#{Pod::Config.instance.installation_root}/.." 42 | ) 43 | 44 | target 'MicroBlog_Share' do 45 | inherit! :complete 46 | end 47 | 48 | post_install do |installer| 49 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 50 | react_native_post_install( 51 | installer, 52 | config[:reactNativePath], 53 | :mac_catalyst_enabled => false 54 | ) 55 | 56 | installer.pods_project.targets.each do |target| 57 | target.build_configurations.each do |config| 58 | config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/screens/stacks/DiscoverStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import DiscoverScreen from '../discover/discover'; 6 | import ProfileImage from './../../components/header/profile_image'; 7 | import NewPostButton from '../../components/header/new_post'; 8 | import BackButton from '../../components/header/back'; 9 | import DiscoverTopicScreen from '../../screens/discover/topic'; 10 | import { getSharedScreens } from './SharedStack' 11 | import App from '../../stores/App' 12 | 13 | const DiscoverStack = createNativeStackNavigator(); 14 | 15 | @observer 16 | export default class Discover extends React.Component{ 17 | 18 | render() { 19 | const sharedScreens = getSharedScreens(DiscoverStack, "Discover") 20 | return( 21 | 28 | ({ 32 | headerLeft: () => , 33 | headerRight: () => 34 | })} 35 | /> 36 | ({ 38 | headerLeft: () => , 39 | headerBackTitleVisible: false 40 | })} 41 | > 42 | {sharedScreens} 43 | ({ 47 | headerTitle: `${route.params?.topic.emoji} ${route.params?.topic.title}`, 48 | headerRight: () => 49 | })} 50 | /> 51 | 52 | 53 | ) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/components/sheets/tagmoji.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity } from 'react-native'; 4 | import { ScrollView } from 'react-native-gesture-handler'; 5 | import ActionSheet from "react-native-actions-sheet"; 6 | import Discover from '../../stores/Discover' 7 | import App from '../../stores/App' 8 | import SheetHeader from "./header"; 9 | 10 | @observer 11 | export default class TagmojiMenu extends React.Component{ 12 | 13 | _return_tagmoji_menu() { 14 | return ( 15 | 23 | { 24 | Discover.tagmoji.map((tagmoji, index) => { 25 | return ( 26 | { App.close_sheet("tagmoji_menu"); App.navigate_to_screen("discover/topic", tagmoji) }} 35 | > 36 | {tagmoji.emoji} 37 | {tagmoji.name} 38 | 39 | ) 40 | } 41 | ) 42 | } 43 | 44 | ) 45 | } 46 | 47 | render() { 48 | return( 49 | 60 | 61 | 67 | {this._return_tagmoji_menu()} 68 | 69 | 70 | ) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/stores/models/Notification.js: -------------------------------------------------------------------------------- 1 | import { types, flow, destroy } from 'mobx-state-tree'; 2 | import MicroBlogApi from '../../api/MicroBlogApi' 3 | import App from '../App' 4 | import Auth from '../Auth' 5 | import Push from '../Push' 6 | 7 | export default Notification = types.model('Notification', { 8 | id: types.identifier, 9 | post_id: types.maybeNull(types.string), 10 | message: types.maybeNull(types.string), 11 | from_username: types.maybeNull(types.string), 12 | from_avatar_url: types.maybeNull(types.string), 13 | to_username: types.maybeNull(types.string), 14 | should_open: types.maybeNull(types.boolean), 15 | did_load_before_user_was_loaded: types.maybeNull(types.boolean) 16 | }) 17 | .actions(self => ({ 18 | 19 | hydrate: flow(function* () { 20 | console.log("Notification:hydrate") 21 | if (self.from_username) { 22 | const profile = yield MicroBlogApi.get_profile(self.from_username) 23 | if (profile) { 24 | console.log("Notification:hydrate:profile", profile.author?.avatar) 25 | self.from_avatar_url = profile.author?.avatar 26 | } 27 | } 28 | }), 29 | 30 | afterCreate: flow(function* () { 31 | if (self.should_open != null && self.should_open) { 32 | self.handle_action() 33 | } 34 | else{ 35 | self.hydrate() 36 | } 37 | }), 38 | 39 | handle_action: flow(function* () { 40 | console.log("Notification:handle_action", self, self.local_user()) 41 | if (self.local_user() != null) { 42 | if (Auth.selected_user !== self.local_user()) { 43 | yield Auth.select_user(self.local_user()) 44 | } 45 | App.navigate_to_screen("open", self.post_id) 46 | Push.close_notification_sheet() 47 | } 48 | }), 49 | 50 | remove: flow(function* () { 51 | destroy(self) 52 | }) 53 | 54 | })) 55 | .views(self => ({ 56 | 57 | local_user() { 58 | return Auth.users.find(u => u.username === self.to_username) 59 | }, 60 | 61 | can_show_notification() { 62 | return self.message && this.local_user() != null && !self.should_open 63 | }, 64 | 65 | trimmed_message() { 66 | if (self.message.length > 255) { 67 | return `${self.message.slice(0, 250)}...` 68 | } 69 | return self.message 70 | } 71 | 72 | })) 73 | -------------------------------------------------------------------------------- /src/screens/replies/edit.android.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TextInput } from 'react-native'; 4 | import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; 5 | import ReplyToolbar from '../../components/keyboard/reply_toolbar' 6 | import App from '../../stores/App' 7 | import Replies from '../../stores/Replies' 8 | import LoadingComponent from '../../components/generic/loading'; 9 | 10 | @observer 11 | export default class ReplyEditScreen extends React.Component{ 12 | 13 | render() { 14 | return( 15 | <> 16 | 17 | 0 ? 90 : 50, 25 | }} 26 | editable={!Replies.selected_reply?.is_sending_reply} 27 | multiline={true} 28 | scrollEnabled={true} 29 | returnKeyType={'default'} 30 | keyboardType={'default'} 31 | autoFocus={true} 32 | autoCorrect={true} 33 | clearButtonMode={'while-editing'} 34 | enablesReturnKeyAutomatically={true} 35 | underlineColorAndroid={'transparent'} 36 | value={Replies.selected_reply?.reply_text} 37 | onChangeText={(text) => !Replies.selected_reply?.is_sending_reply ? Replies.selected_reply.set_reply_text(text) : null} 38 | onSelectionChange={({ nativeEvent: { selection } }) => { 39 | Replies.selected_reply?.set_text_selection(selection) 40 | }} 41 | /> 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/screens/replies/replies.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, ScrollView, RefreshControl } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import LoginMessage from '../../components/info/login_message' 6 | import App from '../../stores/App' 7 | import Replies from '../../stores/Replies'; 8 | import ReplyCell from '../../components/cells/reply_cell'; 9 | 10 | @observer 11 | export default class RepliesScreen extends React.Component{ 12 | 13 | componentDidMount() { 14 | Replies.hydrate() 15 | } 16 | 17 | _return_header = () => { 18 | return( 19 | 30 | Replies can be edited in the first 24 hours after posting. 31 | 32 | ) 33 | } 34 | 35 | _return_replies_list = () => { 36 | return( 37 | 48 | } 49 | > 50 | { 51 | Replies.replies.map((reply) => { 52 | return 53 | }) 54 | } 55 | 56 | ) 57 | } 58 | 59 | render() { 60 | return( 61 | 62 | { 63 | Auth.is_logged_in() && !Auth.is_selecting_user ? 64 | <> 65 | {this._return_header()} 66 | {this._return_replies_list()} 67 | > 68 | : 69 | 70 | } 71 | 72 | ) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/components/settings/user_muting.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { View, Text, TouchableOpacity, Platform } from "react-native"; 4 | import { SFSymbol } from "react-native-sfsymbols"; 5 | import { SvgXml } from "react-native-svg"; 6 | import App from "../../stores/App"; 7 | import Auth from "../../stores/Auth"; 8 | import MBImage from "../common/MBImage"; 9 | 10 | @observer 11 | export default class UserMutingSettings extends React.Component { 12 | render() { 13 | const { user, index } = this.props; 14 | return ( 15 | App.navigate_to_screen("muting", user)} 17 | style={{ 18 | width: "100%", 19 | flexDirection: "row", 20 | justifyContent: "space-between", 21 | alignItems: "center", 22 | paddingVertical: 10, 23 | borderBottomWidth: Auth.users.length - 1 !== index ? 1 : 0, 24 | borderColor: App.theme_border_color(), 25 | }} 26 | > 27 | 28 | 33 | 34 | @{user.username} 35 | 36 | 37 | 38 | {Platform.OS === "ios" ? ( 39 | 44 | ) : ( 45 | 50 | )} 51 | 52 | 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/settings/user_posting.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { View, Text, TouchableOpacity } from "react-native"; 4 | import App from "../../stores/App"; 5 | import Auth from "../../stores/Auth"; 6 | import { SvgXml } from "react-native-svg"; 7 | import MBImage from "../common/MBImage"; 8 | 9 | @observer 10 | export default class UserPostingSettings extends React.Component { 11 | render() { 12 | const { user, index } = this.props; 13 | return ( 14 | 23 | 30 | 31 | 41 | 42 | App.navigate_to_screen("post_service", user)} 44 | style={{ flexDirection: "row", alignItems: "center" }} 45 | > 46 | 47 | {user.posting?.selected_service?.description()} 48 | 49 | 52 | 53 | 54 | `} 55 | /> 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/screens/replies/edit.ios.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TextInput } from 'react-native'; 4 | import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; 5 | import ReplyToolbar from '../../components/keyboard/reply_toolbar' 6 | import App from '../../stores/App' 7 | import Replies from '../../stores/Replies' 8 | import LoadingComponent from '../../components/generic/loading'; 9 | 10 | @observer 11 | export default class ReplyEditScreen extends React.Component{ 12 | 13 | render() { 14 | return( 15 | <> 16 | 17 | 280 ? 150 : 0, 28 | }} 29 | editable={!Replies.selected_reply?.is_sending_reply} 30 | multiline={true} 31 | scrollEnabled={true} 32 | returnKeyType={'default'} 33 | keyboardType={'default'} 34 | autoFocus={true} 35 | autoCorrect={true} 36 | clearButtonMode={'while-editing'} 37 | enablesReturnKeyAutomatically={true} 38 | underlineColorAndroid={'transparent'} 39 | value={Replies.selected_reply?.reply_text} 40 | onChangeText={(text) => !Replies.selected_reply?.is_sending_reply ? Replies.selected_reply.set_reply_text(text) : null} 41 | onSelectionChange={({ nativeEvent: { selection } }) => { 42 | Replies.selected_reply?.set_text_selection(selection) 43 | }} 44 | /> 45 | 46 | 47 | 48 | 49 | 50 | > 51 | ) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /android/app/src/main/java/blog/micro/android/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package blog.micro.android; 2 | import android.content.res.Configuration 3 | import expo.modules.ApplicationLifecycleDispatcher 4 | import expo.modules.ReactNativeHostWrapper 5 | 6 | import android.app.Application 7 | import com.facebook.react.PackageList 8 | import com.facebook.react.ReactApplication 9 | import com.facebook.react.ReactHost 10 | import com.facebook.react.ReactNativeHost 11 | import com.facebook.react.ReactPackage 12 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 13 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 14 | import com.facebook.react.defaults.DefaultReactNativeHost 15 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 16 | import com.facebook.soloader.SoLoader 17 | 18 | class MainApplication : Application(), ReactApplication { 19 | 20 | override val reactNativeHost: ReactNativeHost = 21 | ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { 22 | override fun getPackages(): List = 23 | PackageList(this).packages.apply { 24 | // Packages that cannot be autolinked yet can be added manually here, for example: 25 | // add(MyReactNativePackage()) 26 | } 27 | 28 | override fun getJSMainModuleName(): String = "index" 29 | 30 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 31 | 32 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 33 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 34 | }) 35 | 36 | override val reactHost: ReactHost 37 | get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) 38 | 39 | override fun onCreate() { 40 | super.onCreate() 41 | SoLoader.init(this, OpenSourceMergedSoMapping) 42 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 43 | // If you opted-in for the New Architecture, we load the native entry point for this app. 44 | load() 45 | } 46 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 47 | } 48 | 49 | override fun onConfigurationChanged(newConfig: Configuration) { 50 | super.onConfigurationChanged(newConfig) 51 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/header/back.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass, STANDARD_SLOP } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | import { HeaderBackButton } from '@react-navigation/elements'; 9 | import { useNavigation } from '@react-navigation/native'; 10 | 11 | const BackButtonContent = observer(() => { 12 | const navigation = useNavigation(); 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style = { 17 | marginRight: 0 - STANDARD_SLOP, 18 | marginLeft: 16 - STANDARD_SLOP, 19 | marginTop: 2 - STANDARD_SLOP, 20 | marginBottom: 0 - STANDARD_SLOP, 21 | padding: STANDARD_SLOP 22 | } 23 | } 24 | else { 25 | button_style = { 26 | marginLeft: -20, 27 | marginRight: 0, 28 | marginTop: -10, 29 | marginBottom: -10, 30 | paddingLeft: 20, 31 | paddingRight: 0, 32 | paddingTop: 10, 33 | paddingBottom: 10 34 | }; 35 | } 36 | 37 | return ( 38 | { 40 | if (navigation.canGoBack()) { 41 | navigation.goBack() 42 | } 43 | }} 44 | style={button_style} 45 | labelVisible={false} 46 | hitSlop={{ top: 5, bottom: 5, left: 5, right: 5 }} 47 | backImage={() => ( 48 | Platform.OS === 'ios' ? 49 | 54 | : 55 | 67 | )} 68 | /> 69 | ) 70 | }) 71 | 72 | @observer 73 | export default class BackButton extends React.Component { 74 | render() { 75 | return 76 | } 77 | } -------------------------------------------------------------------------------- /src/screens/stacks/BookmarksStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform, View } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import BookmarksScreen from '../bookmarks/bookmarks'; 6 | import ProfileImage from './../../components/header/profile_image'; 7 | import AddBookmarkButton from '../../components/header/add_bookmark'; 8 | import BookmarkScreen from '../bookmarks/bookmark'; 9 | import NewPostButton from '../../components/header/new_post'; 10 | import { getSharedScreens } from './SharedStack' 11 | import TagsButton from '../../components/header/tags_button'; 12 | import BackButton from '../../components/header/back'; 13 | import App from '../../stores/App' 14 | 15 | const BookmarksStack = createNativeStackNavigator(); 16 | 17 | @observer 18 | export default class Bookmarks extends React.Component{ 19 | 20 | render() { 21 | const sharedScreens = getSharedScreens(BookmarksStack, "Bookmarks") 22 | return( 23 | 30 | ({ 34 | headerLeft: () => , 35 | headerRight: () => ( 36 | 37 | 38 | 39 | 40 | ) 41 | })} 42 | /> 43 | ({ 45 | headerLeft: () => , 46 | headerBackTitleVisible: false 47 | })} 48 | > 49 | {sharedScreens} 50 | ({ 54 | headerTitle: `Bookmark`, 55 | headerRight: () => 56 | })} 57 | /> 58 | 59 | 60 | ) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/components/bookmarks/tag_filter_header.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | import { SvgXml } from 'react-native-svg'; 7 | import { SFSymbol } from "react-native-sfsymbols"; 8 | 9 | @observer 10 | export default class TagFilterHeader extends React.Component{ 11 | 12 | render() { 13 | if(Auth.selected_user?.selected_tag != null){ 14 | return( 15 | 16 | 17 | Auth.selected_user?.set_selected_tag(null)} 31 | > 32 | { 33 | Platform.OS === "ios" ? 34 | 39 | : 40 | 51 | } 52 | 53 | tag: {Auth.selected_user?.selected_tag} 54 | 55 | 56 | ) 57 | } 58 | return null 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/components/keyboard/username_toolbar.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { 4 | View, 5 | Text, 6 | TouchableOpacity, 7 | Image, 8 | Platform, 9 | ScrollView, 10 | } from "react-native"; 11 | import App from "../../stores/App"; 12 | 13 | @observer 14 | export default class UsernameToolbar extends React.Component { 15 | render() { 16 | if (App.found_users.length == 0) { 17 | return null; 18 | } else { 19 | return ( 20 | 30 | 40 | {App.found_users.map((u, index) => { 41 | return ( 42 | { 51 | App.update_autocomplete(u.username, this.props.object); 52 | }} 53 | > 54 | 63 | 70 | @{u.username} 71 | 72 | 73 | ); 74 | })} 75 | 76 | 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/share/header.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { View, Text, TouchableOpacity, Platform } from 'react-native' 4 | import Share from '../../stores/Share' 5 | import App from '../../stores/App' 6 | import { SvgXml } from 'react-native-svg' 7 | 8 | @observer 9 | export default class ShareHeaderComponent extends React.Component { 10 | 11 | render() { 12 | const header_base_style = { 13 | paddingVertical: 15, 14 | paddingHorizontal: 8, 15 | borderBottomWidth: 1, 16 | borderBottomColor: App.theme_border_color(), 17 | flexDirection: 'row', 18 | justifyContent: 'space-between' 19 | } 20 | 21 | let header_ios_style = {}; 22 | if ((Platform.OS == 'ios') && (parseInt(Platform.Version, 10) >= 26)) { 23 | // Liquid Glass 24 | header_ios_style = { 25 | marginTop: 3, 26 | marginLeft: 7, 27 | marginRight: 7 28 | } 29 | } 30 | 31 | return ( 32 | 33 | { 34 | Share.image_options_open ? 35 | 36 | 38 | 39 | `} 40 | width="24" 41 | height="24" 42 | /> 43 | 44 | : 45 | 46 | `} 48 | width="24" 49 | height="24" 50 | /> 51 | 52 | } 53 | { 54 | !Share.image_options_open && 55 | 56 | { 57 | Share.can_save_as_bookmark() && 58 | 59 | Save Bookmark 60 | 61 | } 62 | 63 | Post 64 | 65 | 66 | } 67 | 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/screens/share/post.android.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { View, Text } from 'react-native' 4 | import { TextInput } from 'react-native'; 5 | import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; 6 | import Share from '../../stores/Share' 7 | import App from '../../stores/App' 8 | import Auth from '../../stores/Auth'; 9 | import AssetToolbar from '../../components/keyboard/asset_toolbar' 10 | import PostToolbar from '../../components/keyboard/post_toolbar' 11 | 12 | @observer 13 | export default class SharePostScreen extends React.Component { 14 | 15 | render() { 16 | const { selected_user } = Auth 17 | return ( 18 | Share.is_logged_in() && selected_user?.posting != null ? 19 | <> 20 | 21 | { 22 | Share.error_message != null && 23 | 24 | {Share.error_message} 25 | 26 | } 27 | { 28 | Share.can_save_as_bookmark() && 29 | 30 | 31 | You can save this URL as a bookmark or keep editing to create a new post. 32 | 33 | 34 | } 35 | !selected_user?.posting.is_sending_post ? Share.set_post_text(text) : null} 56 | onSelectionChange={({ nativeEvent: { selection } }) => { 57 | Share.set_text_selection(selection) 58 | }} 59 | /> 60 | 61 | 62 | 63 | 64 | 65 | > 66 | : null 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/screens/stacks/TabNavigator.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 4 | import App from '../../stores/App'; 5 | import TimelineStack from './TimelineStack'; 6 | import MentionsStack from './MentionsStack'; 7 | import BookmarksStack from './BookmarksStack'; 8 | import DiscoverStack from './DiscoverStack'; 9 | import TabIcon from '../../components/tabs/tab'; 10 | 11 | const Tab = createBottomTabNavigator(); 12 | 13 | @observer 14 | export default class TabNavigator extends React.Component { 15 | 16 | render() { 17 | return ( 18 | ({ 22 | tabBarStyle: { 23 | borderTopColor: App.theme_tabbar_divider_color(), 24 | borderTopWidth: 0.5, 25 | backgroundColor: App.theme_navbar_background_color(), 26 | elevation: 0, 27 | shadowOpacity: 0 28 | }, 29 | tabBarIcon: ({ focused, color, size }) => { 30 | return ; 31 | }, 32 | headerShown: false, 33 | tabBarActiveTintColor: App.theme_accent_color() 34 | })} 35 | screenListeners={{ 36 | state: (e) => { 37 | App.set_current_tab_index(e.data.state.index) 38 | }, 39 | focus: (e) => { 40 | App.set_current_tab_key(e.target) 41 | }, 42 | tabPress: (e) => { 43 | App.scroll_web_view_to_top(e.target) 44 | } 45 | }} 46 | > 47 | 55 | 63 | 71 | 79 | 80 | ) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/screens/share/index.android.js: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react' 2 | import * as React from 'react' 3 | import { ActivityIndicator, Button, Text, View, Platform } from 'react-native' 4 | import App from '../../stores/App' 5 | import Share from '../../stores/Share' 6 | import Auth from '../../stores/Auth'; 7 | import SharePostScreen from './post' 8 | import ShareImageOptionsScreen from './image_options' 9 | import ShareHeaderComponent from '../../components/share/header' 10 | 11 | @observer 12 | export default class ShareScreen extends React.Component { 13 | 14 | render() { 15 | return ( 16 | 20 | { 21 | Share.is_loading ? 22 | 23 | 24 | 25 | : 26 | Share.is_logged_in() ? 27 | 28 | 29 | { 30 | Share.image_options_open ? 31 | 32 | : 33 | <> 34 | 35 | { 36 | Auth.selected_user?.posting.is_sending_post || Auth.selected_user?.posting.is_adding_bookmark ? 37 | 48 | 53 | 54 | { Auth.selected_user?.posting.is_sending_post ? "Sending post..." : "Saving bookmark..." } 55 | 56 | 57 | : null 58 | } 59 | > 60 | } 61 | 62 | : 63 | 64 | Using the Micro.blog app, please sign in before using the share extension. 65 | 66 | 67 | } 68 | 69 | ) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Micro.blog 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLSchemes 29 | 30 | microblog 31 | 32 | 33 | 34 | CFBundleVersion 35 | $(CURRENT_PROJECT_VERSION) 36 | ITSAppUsesNonExemptEncryption 37 | 38 | LSRequiresIPhoneOS 39 | 40 | NSAppTransportSecurity 41 | 42 | NSExceptionDomains 43 | 44 | localhost 45 | 46 | NSExceptionAllowsInsecureHTTPLoads 47 | 48 | 49 | 50 | 51 | NSCameraUsageDescription 52 | For taking new photos and posting them. 53 | NSFaceIDUsageDescription 54 | Allow $(PRODUCT_NAME) to access your Face ID biometric data. 55 | NSLocationWhenInUseUsageDescription 56 | The location may be used when posting photos. 57 | NSPhotoLibraryUsageDescription 58 | Micro.blog can upload photos to your microblog. 59 | UIBackgroundModes 60 | 61 | remote-notification 62 | 63 | UILaunchStoryboardName 64 | LaunchScreen 65 | UIRequiredDeviceCapabilities 66 | 67 | arm64 68 | 69 | UISupportedInterfaceOrientations 70 | 71 | UIInterfaceOrientationPortrait 72 | 73 | UISupportedInterfaceOrientations~ipad 74 | 75 | UIInterfaceOrientationLandscapeLeft 76 | UIInterfaceOrientationLandscapeRight 77 | UIInterfaceOrientationPortrait 78 | UIInterfaceOrientationPortraitUpsideDown 79 | 80 | UIViewControllerBasedStatusBarAppearance 81 | 82 | UIDesignRequiresCompatibility 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/screens/share/post.ios.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { InputAccessoryView, View, Text } from 'react-native' 4 | import Share from '../../stores/Share' 5 | import App from '../../stores/App' 6 | import AssetToolbar from '../../components/keyboard/asset_toolbar' 7 | import PostToolbar from '../../components/keyboard/post_toolbar' 8 | import HighlightingText from '../../components/text/highlighting_text'; 9 | 10 | @observer 11 | export default class SharePostScreen extends React.Component { 12 | 13 | constructor (props) { 14 | super(props) 15 | this.input_accessory_view_id = "input_toolbar" 16 | } 17 | 18 | render() { 19 | const { selected_user } = Share 20 | return ( 21 | Share.is_logged_in() && selected_user?.posting != null ? 22 | 23 | { 24 | Share.error_message != null && 25 | 26 | {Share.error_message} 27 | 28 | } 29 | { 30 | Share.can_save_as_bookmark() && 31 | 32 | 33 | You can save this URL as a bookmark or keep editing to create a new post. 34 | 35 | 36 | } 37 | !selected_user?.posting.is_sending_post ? Share.set_post_text(text) : null} 61 | onSelectionChange={({ nativeEvent: { selection } }) => { 62 | Share.set_text_selection(selection) 63 | }} 64 | inputAccessoryViewID={this.input_accessory_view_id} 65 | /> 66 | 67 | 68 | 69 | 70 | 71 | : null 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/screens/stacks/PostingStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, Platform } from 'react-native' 3 | import { observer } from 'mobx-react'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import PostingScreen from '../../screens/posts/new'; 6 | import PostingOptionsScreen from '../../screens/posts/options'; 7 | import CloseModalButton from '../../components/header/close'; 8 | import PostButton from '../../components/header/post_button'; 9 | import RemoveImageButton from '../../components/header/remove_image'; 10 | import ImageOptionsScreen from '../../screens/posts/image_options'; 11 | import UploadsScreen from '../uploads/uploads' 12 | import NewUploadButton from '../../components/header/new_upload'; 13 | import RefreshActivity from '../../components/header/refresh_activity'; 14 | import App from '../../stores/App' 15 | 16 | const PostingStack = createNativeStackNavigator(); 17 | 18 | @observer 19 | export default class Posting extends React.Component{ 20 | 21 | render() { 22 | return( 23 | 34 | , 40 | headerLeft: () => 41 | }} 42 | /> 43 | 50 | ({ 54 | headerTitle: "Image Options", 55 | headerRight: () => 56 | })} 57 | /> 58 | { 64 | return ( 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | }} 72 | /> 73 | 74 | ) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/stores/Discover.js: -------------------------------------------------------------------------------- 1 | import { types, flow } from 'mobx-state-tree'; 2 | import MicroBlogApi, { API_ERROR } from '../api/MicroBlogApi' 3 | 4 | export default Discover = types.model('Discover', { 5 | tagmoji: types.optional(types.array(types.model('Tagmoji', { 6 | name: types.maybeNull(types.string), 7 | title: types.maybeNull(types.string), 8 | emoji: types.maybeNull(types.string), 9 | is_featured: types.maybeNull(types.boolean) 10 | })), []), 11 | random_tagmoji: types.optional(types.array(types.string), []), 12 | search_shown: types.optional(types.boolean, false), 13 | search_query: types.optional(types.string, ""), 14 | search_trigger: types.optional(types.boolean, false), 15 | did_trigger_search: types.optional(types.boolean, false) 16 | }) 17 | .actions(self => ({ 18 | 19 | init: flow(function* () { 20 | console.log("Discover:init") 21 | const tagmoji = yield MicroBlogApi.get_discover_timeline() 22 | if (tagmoji !== API_ERROR && tagmoji != null && tagmoji.length > 0) { 23 | self.tagmoji = tagmoji 24 | self.random_tagmoji = self.random_emojis() 25 | } 26 | }), 27 | 28 | shuffle_random_emoji: flow(function* () { 29 | if (self.tagmoji.length === 0) { 30 | yield self.init() 31 | } 32 | self.random_tagmoji = self.random_emojis() 33 | }), 34 | 35 | toggle_search_bar: flow(function* () { 36 | self.search_shown = !self.search_shown 37 | if(!self.search_shown){ 38 | self.search_query = "" 39 | self.search_trigger = false 40 | self.did_trigger_search = false 41 | } 42 | }), 43 | 44 | set_search_query: flow(function* (value) { 45 | self.search_query = value 46 | if(value === ""){ 47 | self.search_trigger = false 48 | self.did_trigger_search = false 49 | } 50 | }), 51 | 52 | trigger_search: flow(function* (value = true) { 53 | if(value){ 54 | self.did_trigger_search = true 55 | } 56 | self.search_trigger = value 57 | setTimeout(() =>{ 58 | self.trigger_search(false) 59 | }, 50) 60 | 61 | }) 62 | 63 | })) 64 | .views(self => ({ 65 | 66 | random_emojis() { 67 | const emoji_list = self.tagmoji.map(tagmoji => tagmoji.emoji) 68 | const emoji_list_length = emoji_list.length 69 | const emoji_list_random_indexes = [] 70 | for (let i = 0; i < 3; i++) { 71 | let random_index = Math.floor(Math.random() * emoji_list_length) 72 | while (emoji_list_random_indexes.includes(random_index)) { 73 | random_index = Math.floor(Math.random() * emoji_list_length) 74 | } 75 | emoji_list_random_indexes.push(random_index) 76 | } 77 | return emoji_list.filter((_emoji, index) => emoji_list_random_indexes.includes(index)) 78 | }, 79 | 80 | can_show_search(){ 81 | return self.did_trigger_search && self.search_shown && self.search_query !== "" && self.search_query.length >= 3 82 | }, 83 | 84 | should_load_search(){ 85 | return self.search_trigger 86 | }, 87 | 88 | topic_by_slug(slug){ 89 | return self.tagmoji.find(topic => topic.name === slug) 90 | }, 91 | 92 | sanitised_search_query(){ 93 | return self.search_query.trim().replace(/(%20|\s)/g, "+") 94 | } 95 | 96 | })) 97 | .create() 98 | -------------------------------------------------------------------------------- /src/screens/share/index.js: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react' 2 | import * as React from 'react' 3 | import { ActivityIndicator, Button, Text, View, Platform } from 'react-native' 4 | import App from '../../stores/App' 5 | import Share from '../../stores/Share' 6 | import SharePostScreen from './post' 7 | import ShareImageOptionsScreen from './image_options' 8 | import ShareHeaderComponent from '../../components/share/header' 9 | 10 | @observer 11 | export default class ShareScreen extends React.Component { 12 | 13 | componentDidMount() { 14 | console.log('ShareScreen:componentDidMount') 15 | Platform.OS === "ios" && Share.hydrate() 16 | } 17 | 18 | render() { 19 | return ( 20 | 24 | { 25 | Share.is_loading ? 26 | 27 | 28 | { 29 | Platform.OS === "ios" ? 30 | {Share.temp_direct_shared_data} 31 | : null 32 | } 33 | 34 | : 35 | Share.is_logged_in() ? 36 | 37 | 38 | { 39 | Share.image_options_open ? 40 | 41 | : 42 | <> 43 | 44 | { 45 | Share.selected_user?.posting.is_sending_post || Share.selected_user?.posting.is_adding_bookmark ? 46 | 57 | 62 | 63 | { Share.selected_user?.posting.is_sending_post ? "Sending post..." : "Saving bookmark..." } 64 | 65 | 66 | : null 67 | } 68 | > 69 | } 70 | 71 | : 72 | 73 | Using the Micro.blog app, please sign in before using the share extension. 74 | 75 | 76 | } 77 | 78 | ) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.0.5.1) 9 | concurrent-ruby (~> 1.0, >= 1.0.2) 10 | i18n (>= 1.6, < 2) 11 | minitest (>= 5.1) 12 | tzinfo (~> 2.0) 13 | addressable (2.8.7) 14 | public_suffix (>= 2.0.2, < 7.0) 15 | algoliasearch (1.27.5) 16 | httpclient (~> 2.8, >= 2.8.3) 17 | json (>= 1.5.1) 18 | atomos (0.1.3) 19 | base64 (0.2.0) 20 | claide (1.1.0) 21 | cocoapods (1.14.3) 22 | addressable (~> 2.8) 23 | claide (>= 1.0.2, < 2.0) 24 | cocoapods-core (= 1.14.3) 25 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 26 | cocoapods-downloader (>= 2.1, < 3.0) 27 | cocoapods-plugins (>= 1.0.0, < 2.0) 28 | cocoapods-search (>= 1.0.0, < 2.0) 29 | cocoapods-trunk (>= 1.6.0, < 2.0) 30 | cocoapods-try (>= 1.1.0, < 2.0) 31 | colored2 (~> 3.1) 32 | escape (~> 0.0.4) 33 | fourflusher (>= 2.3.0, < 3.0) 34 | gh_inspector (~> 1.0) 35 | molinillo (~> 0.8.0) 36 | nap (~> 1.0) 37 | ruby-macho (>= 2.3.0, < 3.0) 38 | xcodeproj (>= 1.23.0, < 2.0) 39 | cocoapods-core (1.14.3) 40 | activesupport (>= 5.0, < 8) 41 | addressable (~> 2.8) 42 | algoliasearch (~> 1.0) 43 | concurrent-ruby (~> 1.1) 44 | fuzzy_match (~> 2.0.4) 45 | nap (~> 1.0) 46 | netrc (~> 0.11) 47 | public_suffix (~> 4.0) 48 | typhoeus (~> 1.0) 49 | cocoapods-deintegrate (1.0.5) 50 | cocoapods-downloader (2.1) 51 | cocoapods-plugins (1.0.0) 52 | nap 53 | cocoapods-search (1.0.1) 54 | cocoapods-trunk (1.6.0) 55 | nap (>= 0.8, < 2.0) 56 | netrc (~> 0.11) 57 | cocoapods-try (1.2.0) 58 | colored2 (3.1.2) 59 | concurrent-ruby (1.2.2) 60 | escape (0.0.4) 61 | ethon (0.16.0) 62 | ffi (>= 1.15.0) 63 | ffi (1.17.0) 64 | fourflusher (2.3.1) 65 | fuzzy_match (2.0.4) 66 | gh_inspector (1.1.3) 67 | httpclient (2.8.3) 68 | i18n (1.14.1) 69 | concurrent-ruby (~> 1.0) 70 | json (2.7.2) 71 | minitest (5.18.1) 72 | molinillo (0.8.0) 73 | nanaimo (0.3.0) 74 | nap (1.1.0) 75 | netrc (0.11.0) 76 | nkf (0.2.0) 77 | public_suffix (4.0.7) 78 | rexml (3.2.9) 79 | strscan 80 | ruby-macho (2.5.1) 81 | strscan (3.1.0) 82 | typhoeus (1.4.1) 83 | ethon (>= 0.9.0) 84 | tzinfo (2.0.6) 85 | concurrent-ruby (~> 1.0) 86 | xcodeproj (1.24.0) 87 | CFPropertyList (>= 2.3.3, < 4.0) 88 | atomos (~> 0.1.3) 89 | claide (>= 1.0.2, < 2.0) 90 | colored2 (~> 3.1) 91 | nanaimo (~> 0.3.0) 92 | rexml (~> 3.2.4) 93 | 94 | PLATFORMS 95 | ruby 96 | 97 | DEPENDENCIES 98 | activesupport (>= 6.1.7.5, != 7.1.0) 99 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0) 100 | concurrent-ruby (<= 1.3.4) 101 | xcodeproj (< 1.26.0) 102 | 103 | RUBY VERSION 104 | ruby 3.0.4p208 105 | 106 | BUNDLED WITH 107 | 2.7.2 108 | -------------------------------------------------------------------------------- /src/components/sheets/posts_destination.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity, Image, ScrollView } from 'react-native'; 4 | import ActionSheet, { SheetManager } from "react-native-actions-sheet"; 5 | import Auth from '../../stores/Auth'; 6 | import App from '../../stores/App' 7 | import CheckmarkIcon from '../../assets/icons/checkmark.png'; 8 | 9 | @observer 10 | export default class PostsDestinationMenu extends React.Component{ 11 | 12 | constructor(props){ 13 | super(props); 14 | this.sheetId = props.sheetId 15 | } 16 | 17 | _render_destinations = () => { 18 | const { selected_service } = Auth.selected_user.posting 19 | const { config } = selected_service 20 | return config.destination.map((destination, index, array) => { 21 | const is_last = index === array.length - 1; 22 | const is_selected_blog = config.posts_destination() === destination 23 | return( 24 | { 27 | selected_service.set_active_destination(destination, this.props.payload?.type); 28 | SheetManager.hide(this.sheetId); 29 | }} 30 | style={{ 31 | flexDirection: "row", 32 | justifyContent: "space-between", 33 | alignItems: "center", 34 | paddingVertical: 15, 35 | borderBottomWidth: is_last ? 0 : 1, 36 | borderColor: App.theme_border_color() 37 | }} 38 | > 39 | 40 | {destination.name} 41 | 42 | { 43 | is_selected_blog ? 44 | 45 | : null 46 | } 47 | 48 | ) 49 | }) 50 | } 51 | 52 | render() { 53 | if (Auth.selected_user == null) { return null; } 54 | return( 55 | 63 | 64 | 73 | Blogs 74 | 75 | {this._render_destinations()} 76 | 77 | 78 | 79 | 80 | ) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/screens/bookmarks/add_bookmark.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TextInput, Button, ActivityIndicator, Keyboard, Platform } from 'react-native'; 4 | import { KeyboardAvoidingView } from "react-native-keyboard-controller"; 5 | import App from '../../stores/App' 6 | 7 | @observer 8 | export default class AddBookmarkScreen extends React.Component{ 9 | 10 | constructor (props) { 11 | super(props) 12 | this.state = { 13 | url: "" 14 | } 15 | this._input_ref = React.createRef() 16 | } 17 | 18 | _dismiss = () => { 19 | Keyboard.dismiss() 20 | App.go_back() 21 | } 22 | 23 | _add_bookmark = async () => { 24 | const bookmark = await Auth.selected_user.posting.add_bookmark(this.state.url) 25 | console.log("AddBookmarkScreen:_add_bookmark", bookmark) 26 | if (bookmark) { 27 | this.setState({ url: "" }) 28 | this._input_ref.current.clear() 29 | this._dismiss() 30 | } 31 | } 32 | 33 | render() { 34 | const { posting } = Auth.selected_user 35 | return( 36 | 37 | 38 | 39 | For Micro.blog Premium subscribers, bookmarked web pages are also archived so you can read them later and make highlights. 40 | 41 | !posting.is_adding_bookmark ? this.setState({url: text}) : null} 69 | /> 70 | 76 | 82 | 83 | 84 | ) 85 | } 86 | 87 | } 88 | --------------------------------------------------------------------------------
This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package blog.micro.android; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } -------------------------------------------------------------------------------- /src/screens/timeline/timeline.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen'; 5 | import App from '../../stores/App' 6 | 7 | 8 | @observer 9 | export default class TimelineScreen extends React.Component{ 10 | 11 | render() { 12 | return( 13 | <> 14 | 20 | > 21 | ) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/string_utils.js: -------------------------------------------------------------------------------- 1 | String.prototype.InsertTextStyle = function (value_to_insert, selection, is_link = false, url = null) { 2 | const start = selection.start 3 | const end = selection.end + 1 4 | let result = '' 5 | 6 | if (start === end) { 7 | result = `${this.slice(0, start)}${value_to_insert}${this.slice(end - 1)}` 8 | } 9 | else { 10 | const beginning_text = this.slice(0, start) 11 | const selected_text = this.slice(start, end - 1) 12 | const remaining_text = this.slice(end - 1) 13 | 14 | if (is_link) { 15 | const link_text = `[${selected_text}](${url || ''})` 16 | result = `${beginning_text}${link_text}${remaining_text}` 17 | } 18 | else { 19 | result = `${beginning_text}${value_to_insert}${selected_text}${value_to_insert}${remaining_text}` 20 | } 21 | } 22 | 23 | return result; 24 | } -------------------------------------------------------------------------------- /src/components/header/update_page.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from '../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | 7 | @observer 8 | export default class UpdatePageButton extends React.Component { 9 | 10 | render() { 11 | return ( 12 | { 16 | const sent = await Auth.selected_user.posting.send_update_post() 17 | if (sent) { 18 | Auth.selected_user.posting.clear_post() 19 | Keyboard.dismiss() 20 | Auth.selected_user.posting.selected_service.update_pages_for_active_destination() 21 | App.go_back() 22 | } 23 | }} 24 | /> 25 | ) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/utils/dev.js: -------------------------------------------------------------------------------- 1 | import { LogBox } from 'react-native'; 2 | 3 | LogBox.ignoreLogs([ 4 | "Error: Nothing to dismiss", 5 | "Can't perform a React state update on an unmounted component", 6 | "Error: A stack can't contain two children with the same id", 7 | "Cannot record touch end without", 8 | "Require cycle:", 9 | "onAnimatedValueUpdate", 10 | "Cannot update during an existing state transition", 11 | "You are trying to read or write to an object that is no longer part of a state tree", 12 | "Cannot read property 'addNodeToCache' of undefined", 13 | "the creation of the observable instance must be done on the initializing phase", 14 | "`new NativeEventEmitter()` was called with a non-null argument without the required", 15 | "Sending `onAnimatedValueUpdate` with no listeners registered", 16 | "Did not receive response to shouldStartLoad", 17 | "Open debugger to view warnings" 18 | ]) 19 | -------------------------------------------------------------------------------- /src/components/header/screen_title.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, Platform } from 'react-native'; 4 | import ProfileImage from './profile_image'; 5 | import App from '../../stores/App' 6 | 7 | @observer 8 | export default class ScreenTitle extends React.Component{ 9 | 10 | render() { 11 | if (this.props.title) { 12 | return ( 13 | 20 | { Platform.OS === 'android' && } 21 | {this.props.title} 22 | 23 | ) 24 | } 25 | return null 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/color_syntax_tags.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.540", 9 | "green" : "0.150", 10 | "red" : "0.590" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.930", 27 | "green" : "0.670", 28 | "red" : "0.890" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Images.xcassets/color_editing_paragraph.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/web/error_view.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { ActivityIndicator, View, Text } from "react-native" 4 | import App from '../../stores/App' 5 | 6 | @observer 7 | export default class WebErrorViewModule extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 12 | Whoops, an error occured. 13 | {this.props.error_name} 14 | Please pull to refresh to try again... 15 | 16 | ) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/screens/following/following.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen' 5 | 6 | @observer 7 | export default class FollowingScreen extends React.Component{ 8 | 9 | render() { 10 | return ( 11 | 23 | ) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/components/sheets/login_message.js: -------------------------------------------------------------------------------- 1 | import ActionSheet from "react-native-actions-sheet"; 2 | import * as React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | import { SafeAreaView, Text } from 'react-native'; 5 | import Login from './../../stores/Login'; 6 | 7 | @observer 8 | export default class LoginMessageSheet extends React.Component { 9 | render() { 10 | return ( 11 | 21 | 22 | {Login.message} 23 | 24 | 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/components/header/update_post.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Keyboard } from 'react-native'; 4 | import App from '../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | 7 | @observer 8 | export default class UpdatePostButton extends React.Component { 9 | 10 | render() { 11 | const { post_status } = Auth.selected_user?.posting; 12 | return ( 13 | { 18 | const sent = await Auth.selected_user.posting.send_update_post() 19 | if (sent) { 20 | Auth.selected_user.posting.clear_post() 21 | Keyboard.dismiss() 22 | Auth.selected_user.posting.selected_service.update_posts_for_active_destination() 23 | App.go_back() 24 | } 25 | }} 26 | /> 27 | ) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | buildToolsVersion = "35.0.0" 5 | minSdkVersion = 24 6 | compileSdkVersion = 35 7 | targetSdkVersion = 35 8 | ndkVersion = "27.1.12297006" 9 | kotlinVersion = "2.0.21" 10 | androidXBrowser = "1.8.0" 11 | } 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | dependencies { 17 | classpath("com.android.tools.build:gradle") 18 | classpath("com.facebook.react:react-native-gradle-plugin") 19 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 20 | classpath 'com.google.gms:google-services:4.3.14' 21 | } 22 | gradle.startParameter.excludedTaskNames.addAll( 23 | gradle.startParameter.taskNames.findAll { it.contains("testClasses") } 24 | ) 25 | } 26 | 27 | apply plugin: "com.facebook.react.rootproject" 28 | apply plugin: "expo-root-project" 29 | -------------------------------------------------------------------------------- /src/screens/conversation/conversation.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from '../../stores/Auth'; 4 | import Reply from '../../stores/Reply' 5 | import GenericScreenComponent from '../../components/generic/generic_screen' 6 | import App from '../../stores/App' 7 | 8 | @observer 9 | export default class ConversationScreen extends React.Component{ 10 | 11 | render() { 12 | return ( 13 | 20 | ) 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /android/app/src/main/java/blog/micro/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package blog.micro.android; 2 | import expo.modules.ReactActivityDelegateWrapper 3 | 4 | import com.facebook.react.ReactActivity 5 | import com.facebook.react.ReactActivityDelegate 6 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 7 | import com.facebook.react.defaults.DefaultReactActivityDelegate 8 | 9 | 10 | class MainActivity : ReactActivity() { 11 | 12 | 13 | 14 | /** 15 | * Returns the name of the main component registered from JavaScript. This is used to schedule 16 | * rendering of the component. 17 | */ 18 | override fun getMainComponentName(): String = "Micro.blog" 19 | 20 | /** 21 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 22 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 23 | */ 24 | override fun createReactActivityDelegate(): ReactActivityDelegate = 25 | ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)) 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Micro.blog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stores/Settings.js: -------------------------------------------------------------------------------- 1 | import { types, flow, applySnapshot } from 'mobx-state-tree'; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | 4 | export default Settings = types.model('Settings', { 5 | open_links_in_external_browser: types.optional(types.boolean, false), 6 | open_links_with_reader_mode: types.optional(types.boolean, false) 7 | }) 8 | .actions(self => ({ 9 | 10 | hydrate: flow(function* () { 11 | console.log("Settings:hydrate") 12 | const data = yield AsyncStorage.getItem('Settings') 13 | if (data) { 14 | applySnapshot(self, JSON.parse(data)) 15 | console.log("Settings:hydrate:with_data") 16 | } 17 | }), 18 | 19 | toggle_open_links_in_external_browser: flow(function* () { 20 | console.log("Settings:toggle_open_links_in_external_browser") 21 | self.open_links_in_external_browser = !self.open_links_in_external_browser 22 | }), 23 | 24 | toggle_open_links_with_reader_mode: flow(function* () { 25 | console.log("Settings:open_links_with_reader_mode") 26 | self.open_links_with_reader_mode = !self.open_links_with_reader_mode 27 | }) 28 | 29 | })) 30 | .create(); -------------------------------------------------------------------------------- /src/components/generic/loading.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, ActivityIndicator, Text } from 'react-native'; 3 | import App from '../../stores/App'; 4 | 5 | export default class LoadingComponent extends React.Component{ 6 | 7 | render() { 8 | const { should_show, message, size = 'large' } = this.props 9 | if (!should_show) return null 10 | return( 11 | 22 | 27 | 28 | { 29 | message && {message} 30 | } 31 | 32 | 33 | ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/components/sheets/sheets.js: -------------------------------------------------------------------------------- 1 | import { registerSheet } from 'react-native-actions-sheet'; 2 | import LoginMessageSheet from './login_message'; 3 | import SheetMenu from './menu'; 4 | import ProfileMoreMenu from "./profile_more" 5 | import TagmojiMenu from "./tagmoji"; 6 | import PostsDestinationMenu from "./posts_destination"; 7 | import TagsMenu from "./tags"; 8 | import AddTagsMenu from "./add_tags"; 9 | import NotificationsSheetsMenu from "./notifications"; 10 | import UploadInfoSheet from "./upload_info"; 11 | import CollectionsSheet from "./collections"; 12 | import ReplySheet from './reply_sheet'; 13 | 14 | registerSheet("login-message-sheet", LoginMessageSheet) 15 | registerSheet("main_sheet", SheetMenu); 16 | registerSheet("profile_more_menu", ProfileMoreMenu); 17 | registerSheet("tagmoji_menu", TagmojiMenu); 18 | registerSheet("posts_destination_menu", PostsDestinationMenu); 19 | registerSheet("tags_menu", TagsMenu) 20 | registerSheet("add_tags_sheet", AddTagsMenu) 21 | registerSheet("notifications_sheet", NotificationsSheetsMenu) 22 | registerSheet("upload_info_sheet", UploadInfoSheet) 23 | registerSheet("collections_sheet", CollectionsSheet) 24 | registerSheet("reply_sheet", ReplySheet) 25 | 26 | export { } 27 | -------------------------------------------------------------------------------- /patches/react-native-image-viewing+0.2.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js 2 | index 0708505..1b9a7cc 100644 3 | --- a/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js 4 | +++ b/node_modules/react-native-image-viewing/dist/components/ImageItem/ImageItem.ios.js 5 | @@ -26,10 +26,10 @@ const ImageItem = ({ imageSrc, onZoom, onRequestClose, onLongPress, delayLongPre 6 | const scrollValueY = new Animated.Value(0); 7 | const scaleValue = new Animated.Value(scale || 1); 8 | const translateValue = new Animated.ValueXY(translate); 9 | - const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1; 10 | + const maxScale = scale && scale > 0 ? 3 : 1; 11 | const imageOpacity = scrollValueY.interpolate({ 12 | inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], 13 | - outputRange: [0.5, 1, 0.5], 14 | + outputRange: [0.95, 1, 0.95], 15 | }); 16 | const imagesStyles = getImageStyles(imageDimensions, translateValue, scaleValue); 17 | const imageStylesWithOpacity = { ...imagesStyles, opacity: imageOpacity }; 18 | -------------------------------------------------------------------------------- /src/components/common/MBImage.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Image as RNImage } from 'react-native' 3 | import { Image as ExpoImage } from 'expo-image' 4 | import App from '../../stores/App' 5 | 6 | const mapContentFitToResizeMode = (contentFit) => { 7 | if (contentFit === 'contain' || contentFit === 'scale-down') { 8 | return 'contain' 9 | } 10 | if (contentFit === 'cover') { 11 | return 'cover' 12 | } 13 | if (contentFit === 'fill') { 14 | return 'stretch' 15 | } 16 | if (contentFit === 'none') { 17 | return 'center' 18 | } 19 | return undefined 20 | } 21 | 22 | const MBImage = React.forwardRef(({ contentFit, ...props }, ref) => { 23 | if (App.is_share_extension) { 24 | const { 25 | transition, 26 | cachePolicy, 27 | placeholder, 28 | ...restProps 29 | } = props 30 | 31 | const { resizeMode, ...nativeProps } = restProps 32 | const finalResizeMode = resizeMode ?? mapContentFitToResizeMode(contentFit) 33 | 34 | return ( 35 | 40 | ) 41 | } 42 | 43 | return ( 44 | 49 | ) 50 | }) 51 | 52 | export default MBImage 53 | -------------------------------------------------------------------------------- /src/screens/bookmarks/bookmarks.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import GenericScreenComponent from '../../components/generic/generic_screen' 5 | import App from './../../stores/App'; 6 | import TagFilterHeader from '../../components/bookmarks/tag_filter_header'; 7 | 8 | @observer 9 | export default class BookmarksScreen extends React.Component{ 10 | 11 | render() { 12 | return ( 13 | <> 14 | { 15 | Auth.is_logged_in() && Auth.selected_user?.selected_tag != null ? 16 | 17 | : null 18 | } 19 | 27 | > 28 | ) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/components/header/new_collection.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import App from '../../stores/App' 6 | import { SFSymbol } from "react-native-sfsymbols"; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class NewCollectionButton extends React.Component{ 11 | 12 | render() { 13 | return ( 14 | { 21 | App.navigate_to_screen("AddCollection"); 22 | }} 23 | > 24 | { Platform.OS === 'ios' ? 25 | 30 | : 31 | 39 | } 40 | 41 | 42 | ) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/components/header/refresh_activity.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { ActivityIndicator, Platform } from 'react-native'; 4 | import Replies from './../../stores/Replies'; 5 | import Auth from './../../stores/Auth'; 6 | import App from './../../stores/App'; 7 | 8 | @observer 9 | export default class RefreshActivity extends React.Component{ 10 | 11 | render() { 12 | let is_loading = false 13 | switch(this.props.type){ 14 | case "posts": 15 | is_loading = Auth.selected_user.posting.selected_service.is_loading_posts || App.is_searching_posts 16 | break; 17 | case "pages": 18 | is_loading = Auth.selected_user.posting.selected_service.is_loading_pages || App.is_searching_pages 19 | break; 20 | case "uploads": 21 | is_loading = Auth.selected_user.posting.selected_service.is_loading_uploads 22 | break; 23 | case "highlights": 24 | is_loading = App.is_loading_highlights 25 | break; 26 | default: 27 | is_loading = Replies.is_loading 28 | } 29 | return( 30 | 31 | ) 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/screens/discover/discover.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Auth from './../../stores/Auth'; 4 | import TagmojiBar from '../../components/discover/tagmoji_bar' 5 | import Discover from '../../stores/Discover' 6 | import GenericScreenComponent from '../../components/generic/generic_screen'; 7 | 8 | @observer 9 | export default class DiscoverScreen extends React.Component{ 10 | 11 | componentDidMount() { 12 | Discover.init() 13 | } 14 | 15 | render() { 16 | return ( 17 | <> 18 | { 19 | Auth.is_logged_in() ? 20 | 21 | : null 22 | } 23 | 30 | > 31 | ) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/components/generic/generic_screen.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View } from 'react-native'; 4 | import LoginMessage from '../info/login_message'; 5 | import ImageModalModule from '../images/image_modal'; 6 | import WebViewModule from '../web/webview_module'; 7 | import App from '../../stores/App'; 8 | 9 | @observer 10 | export default class GenericScreenComponent extends React.Component{ 11 | 12 | render() { 13 | return( 14 | 15 | { 16 | this.props.can_show_web_view != null && this.props.can_show_web_view ? 17 | <> 18 | {this.props.children} 19 | 20 | > 21 | : 22 | !this.props.is_search && !this.props.is_filtered && !App.is_changing_font_scale && !App.is_loading_bookmarks && 23 | } 24 | 25 | 26 | ) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/snapshots.js: -------------------------------------------------------------------------------- 1 | import { onSnapshot } from 'mobx-state-tree'; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | import * as SecureStore from 'expo-secure-store'; 4 | import SFInfo from 'react-native-sensitive-info' 5 | 6 | import Tokens from './../stores/Tokens'; 7 | import Auth from './../stores/Auth'; 8 | import Settings from './../stores/Settings'; 9 | import Share from '../stores/Share' 10 | 11 | function debounce(func, wait) { 12 | let timeout 13 | return function (...args) { 14 | const context = this 15 | clearTimeout(timeout) 16 | timeout = setTimeout(() => func.apply(context, args), wait) 17 | } 18 | } 19 | 20 | const debounce_ms = 1500 21 | 22 | onSnapshot(Tokens, snapshot => { 23 | SFInfo.setItem('Tokens', JSON.stringify(snapshot), {}), 24 | SecureStore.setItem('Tokens', JSON.stringify(snapshot), {}), 25 | console.log("SNAPSHOT:::TOKENS") 26 | }); 27 | onSnapshot(Auth, debounce( 28 | snapshot => { 29 | AsyncStorage.setItem('Auth', JSON.stringify(snapshot)); 30 | console.log("SNAPSHOT:::AUTH") 31 | }, debounce_ms )) 32 | onSnapshot(Settings, snapshot => { AsyncStorage.setItem('Settings', JSON.stringify(snapshot)), console.log("SNAPSHOT:::SETTINGS") }) 33 | onSnapshot(Share, snapshot => { AsyncStorage.setItem('Share', JSON.stringify(snapshot)), console.log("SNAPSHOT:::SHARE") }); 34 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") 2 | def expoPluginsPath = new File( 3 | providers.exec { 4 | workingDir(rootDir) 5 | commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") 6 | }.standardOutput.asText.get().trim(), 7 | "../android/expo-gradle-plugin" 8 | ).absolutePath 9 | includeBuild(expoPluginsPath) 10 | } 11 | plugins { id("com.facebook.react.settings") 12 | id("expo-autolinking-settings") 13 | } 14 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> 15 | def command = [ 16 | 'node', 17 | '--no-warnings', 18 | '--eval', 19 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 20 | 'react-native-config', 21 | '--json', 22 | '--platform', 23 | 'android' 24 | ].toList() 25 | ex.autolinkLibrariesFromCommand(command) 26 | } 27 | rootProject.name = 'Micro.blog' 28 | include ':app' 29 | includeBuild('../node_modules/@react-native/gradle-plugin') 30 | 31 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") 32 | useExpoModules() 33 | expoAutolinking.useExpoVersionCatalog() 34 | includeBuild(expoAutolinking.reactNativeGradlePlugin) 35 | -------------------------------------------------------------------------------- /src/components/cells/checkmark_row_cell.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { View, Text, Platform } from 'react-native' 3 | import App from '../../stores/App' 4 | import { SvgXml } from 'react-native-svg'; 5 | import { SFSymbol } from "react-native-sfsymbols"; 6 | 7 | export default class CheckmarkRowCell extends React.Component { 8 | 9 | constructor (props) { 10 | super(props); 11 | this.state = { 12 | text: props.text, 13 | is_selected: props.is_selected 14 | }; 15 | } 16 | 17 | render() { 18 | const { text, is_selected } = this.props; 19 | return ( 20 | 21 | {text} 22 | { 23 | is_selected && Platform.OS === 'ios' ? 24 | 25 | : is_selected ? 26 | 33 | : null 34 | } 35 | 36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ios/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryFileTimestamp 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | 0A2A.1 21 | 3B52.1 22 | C617.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategoryDiskSpace 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | E174.1 31 | 85F4.1 32 | 33 | 34 | 35 | NSPrivacyAccessedAPIType 36 | NSPrivacyAccessedAPICategorySystemBootTime 37 | NSPrivacyAccessedAPITypeReasons 38 | 39 | 35F9.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/header/remove_image.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Button, Alert } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | import { StackActions } from '@react-navigation/native'; 7 | 8 | @observer 9 | export default class RemoveImageButton extends React.Component { 10 | 11 | _handle_image_remove = () => { 12 | const { posting } = Auth.selected_user 13 | const { asset, index } = this.props; 14 | const existing_index = posting.post_assets?.findIndex(file => file.uri === asset.uri) 15 | if (existing_index > -1) { 16 | Alert.alert( 17 | "Remove upload?", 18 | "Are you sure you want to remove this upload from this post?", 19 | [ 20 | { 21 | text: "Cancel", 22 | style: 'cancel', 23 | }, 24 | { 25 | text: "Remove", 26 | onPress: () => { 27 | this.props.navigation.goBack() 28 | // delay, seems to create problems otherwise 29 | setTimeout(() => { 30 | posting.remove_asset(index) 31 | }, 500); 32 | }, 33 | style: 'destructive' 34 | }, 35 | ], 36 | {cancelable: false}, 37 | ); 38 | } 39 | } 40 | 41 | render() { 42 | return ( 43 | 48 | ) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/components/header/add_bookmark.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import App from '../../stores/App' 6 | import { SFSymbol } from "react-native-sfsymbols"; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class AddBookmarkButton extends React.Component{ 11 | 12 | render() { 13 | if(Auth.selected_user != null && Auth.selected_user.posting?.posting_enabled()){ 14 | return( 15 | App.navigate_to_screen("add_bookmark")} 22 | > 23 | { 24 | Platform.OS === 'ios' ? 25 | 30 | : 31 | 39 | } 40 | 41 | 42 | ) 43 | } 44 | return null 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/components/header/new_post.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Image, Platform } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import PostAddIcon from './../../assets/icons/post_add.png'; 6 | import App from '../../stores/App' 7 | import { isLiquidGlass } from './../../utils/ui'; 8 | import { SFSymbol } from "react-native-sfsymbols"; 9 | 10 | @observer 11 | export default class NewPostButton extends React.Component{ 12 | 13 | render() { 14 | let button_style = { 15 | justifyContent: 'center', 16 | alignItems: 'center' 17 | }; 18 | 19 | if (isLiquidGlass()) { 20 | button_style.paddingLeft = 4; 21 | } 22 | 23 | if(Auth.selected_user != null && Auth.selected_user.posting?.posting_enabled()){ 24 | return ( 25 | App.navigate_to_screen("Posting")} 28 | accessibilityRole="button" 29 | accessibilityLabel="New post" 30 | > 31 | { 32 | Platform.OS === 'ios' ? 33 | 38 | : 39 | 40 | } 41 | 42 | 43 | ) 44 | } 45 | return null 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/components/header/close.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class CloseModalButton extends React.Component { 11 | 12 | render() { 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style.paddingLeft = 7; 17 | button_style.paddingTop = 4; 18 | } 19 | 20 | return ( 21 | App.go_back()} 23 | style={button_style} 24 | > 25 | { 26 | Platform.OS === 'ios' ? 27 | 32 | : 33 | 45 | } 46 | 47 | ) 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /patches/react-native-sensitive-info@5.5.8.patch: -------------------------------------------------------------------------------- 1 | diff --git a/RNSensitiveInfo.js b/RNSensitiveInfo.js 2 | index b3e164c6bb00e1bb885e796f6f402909ecfb6071..34cec63f2020108cac0051861175a08c92aa5817 100644 3 | --- a/RNSensitiveInfo.js 4 | +++ b/RNSensitiveInfo.js 5 | @@ -1,23 +1,23 @@ 6 | -import {NativeModules} from 'react-native'; 7 | +import { NativeModules } from 'react-native'; 8 | 9 | const RNSensitiveInfo = NativeModules.RNSensitiveInfo; 10 | 11 | -module.exports = { 12 | - ...RNSensitiveInfo, 13 | - setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment) { 14 | - if (RNSensitiveInfo.setInvalidatedByBiometricEnrollment == null) { 15 | - return; 16 | - } 17 | +RNSensitiveInfo.setInvalidatedByBiometricEnrollment = ( 18 | + invalidatedByBiometricEnrollment, 19 | +) => { 20 | + if (RNSensitiveInfo.setInvalidatedByBiometricEnrollment == null) { 21 | + return undefined; 22 | + } 23 | 24 | - return RNSensitiveInfo.setInvalidatedByBiometricEnrollment( 25 | - invalidatedByBiometricEnrollment, 26 | - ); 27 | - }, 28 | - cancelFingerprintAuth() { 29 | - if (RNSensitiveInfo.cancelFingerprintAuth == null) { 30 | - return; 31 | - } 32 | + return RNSensitiveInfo.setInvalidatedByBiometricEnrollment( 33 | + invalidatedByBiometricEnrollment, 34 | + ); 35 | +}; 36 | +RNSensitiveInfo.cancelFingerprintAuth = () => { 37 | + if (RNSensitiveInfo.cancelFingerprintAuth == null) { 38 | + return undefined; 39 | + } 40 | 41 | - return RNSensitiveInfo.cancelFingerprintAuth(); 42 | - }, 43 | + return RNSensitiveInfo.cancelFingerprintAuth(); 44 | }; 45 | +export default RNSensitiveInfo; 46 | -------------------------------------------------------------------------------- /src/screens/profile/profile.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import WebViewModule from '../../components/web/webview_module' 6 | import LoginMessage from '../../components/info/login_message' 7 | import ImageModalModule from '../../components/images/image_modal' 8 | import ProfileHeader from '../../components/profile/profile_header' 9 | import MutedMessage from '../../components/info/muted_message' 10 | import App from '../../stores/App' 11 | 12 | @observer 13 | export default class ProfileScreen extends React.Component{ 14 | 15 | render() { 16 | const { username } = this.props.route.params 17 | const is_muted = Auth.selected_user?.muting?.is_muted(username) 18 | const is_blocked = Auth.selected_user?.muting?.blocked_users.some(u => u.username === username) 19 | 20 | return( 21 | 22 | { 23 | Auth.is_logged_in() && !Auth.is_selecting_user && !App.should_reload_web_view() ? 24 | is_muted || is_blocked ? 25 | 26 | : 27 | : null} /> 28 | : 29 | 30 | } 31 | 32 | 33 | ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/components/header/close_post_clear.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class ClosePostClearButton extends React.Component { 11 | 12 | render() { 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style.paddingLeft = 8; 17 | button_style.paddingTop = 5; 18 | } 19 | 20 | return ( 21 | { 23 | App.go_back_and_clear() 24 | }} 25 | style={button_style} 26 | > 27 | { 28 | Platform.OS === 'ios' ? 29 | 34 | : 35 | 47 | } 48 | 49 | ) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/components/header/reply.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | 9 | @observer 10 | export default class ReplyButton extends React.Component { 11 | 12 | render() { 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style = { 17 | marginLeft: 7, 18 | marginTop: 2 19 | } 20 | } 21 | 22 | return ( 23 | { 25 | if(this.props.conversation_id != null){ 26 | App.open_sheet("reply_sheet", { conversation_id: this.props.conversation_id }) 27 | } 28 | }} 29 | style={button_style} 30 | > 31 | { 32 | Platform.OS === 'ios' ? 33 | 38 | : 39 | 47 | } 48 | 49 | ) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/components/images/image_modal.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import ImageView from "react-native-image-viewing"; 4 | import App from "../../stores/App" 5 | import { Platform, SafeAreaView, TouchableOpacity, Image } from 'react-native' 6 | import { SFSymbol } from "react-native-sfsymbols"; 7 | import ArrowBackIcon from './../../assets/icons/arrow_back.png'; 8 | 9 | @observer 10 | export default class ImageModalModule extends React.Component{ 11 | 12 | close_button = () => { 13 | return ( 14 | 15 | 16 | { 17 | Platform.OS === 'ios' ? 18 | 28 | : 29 | 34 | } 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | render() { 42 | if (App.image_modal_is_open) { 43 | return ( 44 | this.close_button()} 50 | doubleTapToZoomEnabled={true} 51 | /> 52 | ) 53 | } 54 | return null 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/screens/stacks/MentionsStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import MentionsScreen from '../mentions/mentions'; 6 | import ProfileImage from './../../components/header/profile_image'; 7 | import NewPostButton from '../../components/header/new_post'; 8 | import BackButton from '../../components/header/back'; 9 | import { getSharedScreens } from './SharedStack' 10 | import App from '../../stores/App' 11 | 12 | const MentionsStack = createNativeStackNavigator(); 13 | 14 | @observer 15 | export default class Mentions extends React.Component{ 16 | 17 | render() { 18 | const sharedScreens = getSharedScreens(MentionsStack, "Mentions") 19 | return( 20 | 27 | ({ 31 | headerLeft: () => , 32 | headerRight: () => 33 | })} 34 | /> 35 | ({ 37 | headerLeft: () => , 38 | headerBackTitleVisible: false 39 | })} 40 | > 41 | {sharedScreens} 42 | 43 | 44 | ) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/screens/stacks/TimelineStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { observer } from 'mobx-react'; 4 | import { Platform } from 'react-native'; 5 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 6 | import TimelineScreen from '../timeline/timeline'; 7 | import ProfileImage from './../../components/header/profile_image'; 8 | import NewPostButton from '../../components/header/new_post'; 9 | import BackButton from '../../components/header/back'; 10 | import { getSharedScreens } from './SharedStack' 11 | import App from '../../stores/App' 12 | 13 | const TimelineStack = createNativeStackNavigator(); 14 | 15 | @observer 16 | export default class Timeline extends React.Component{ 17 | 18 | render() { 19 | const sharedScreens = getSharedScreens(TimelineStack, "Timeline") 20 | return( 21 | 28 | ({ 32 | headerLeft: () => , 33 | headerRight: () => , 34 | })} 35 | /> 36 | ({ 38 | headerLeft: () => , 39 | headerBackTitleVisible: false, 40 | })} 41 | > 42 | {sharedScreens} 43 | 44 | 45 | ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | .kotlin/ 37 | 38 | # node.js 39 | # 40 | node_modules/ 41 | npm-debug.log 42 | yarn-error.log 43 | 44 | # BUCK 45 | buck-out/ 46 | \.buckd/ 47 | *.keystore 48 | !debug.keystore 49 | 50 | # fastlane 51 | # 52 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 53 | # screenshots whenever they are needed. 54 | # For more information about the recommended setup visit: 55 | # https://docs.fastlane.tools/best-practices/source-control/ 56 | 57 | */fastlane/report.xml 58 | */fastlane/Preview.html 59 | */fastlane/screenshots 60 | **/fastlane/test_output 61 | 62 | # Bundle artifact 63 | *.jsbundle 64 | 65 | # Ruby / CocoaPods 66 | # CocoaPods 67 | **/Pods/ 68 | /vendor/bundle/ 69 | 70 | # Temporary files created by Metro to check the health of the file watcher 71 | .metro-health-check* 72 | 73 | # Yarn 74 | .yarn/* 75 | !.yarn/patches 76 | !.yarn/plugins 77 | !.yarn/releases 78 | !.yarn/sdks 79 | !.yarn/versions 80 | 81 | # User generated 82 | .nova 83 | /android/app/release 84 | /android/app/google-services.json 85 | vendor 86 | ios/.xcode.env.local 87 | /vendor/bundle/ 88 | .yarn/install-state.gz 89 | CLAUDE.md 90 | .cursor 91 | # Expo 92 | .expo 93 | dist/ 94 | web-build/ 95 | -------------------------------------------------------------------------------- /src/screens/stacks/PostEditStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import PostEditScreen from '../posts/edit'; 6 | import PostingOptionsScreen from '../../screens/posts/options'; 7 | import ClosePostClearButton from '../../components/header/close_post_clear'; 8 | import UpdatePostButton from '../../components/header/update_post'; 9 | import App from '../../stores/App' 10 | 11 | const PostingEditStack = createNativeStackNavigator(); 12 | 13 | @observer 14 | export default class PostEditStack extends React.Component{ 15 | 16 | render() { 17 | return( 18 | 29 | , 35 | headerLeft: () => 36 | }} 37 | /> 38 | 45 | 46 | ) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/stores/Reporting.js: -------------------------------------------------------------------------------- 1 | import { types, flow } from 'mobx-state-tree'; 2 | import { Alert, ToastAndroid } from 'react-native'; 3 | import MicroBlogApi, { API_ERROR, REPORTING_ERROR } from '../api/MicroBlogApi' 4 | import Toast from 'react-native-simple-toast'; 5 | 6 | export default Reporting = types.model('Reporting', { 7 | is_sending_report: types.optional(types.boolean, false) 8 | }) 9 | .actions(self => ({ 10 | 11 | init: flow(function* () { 12 | console.log("Reporting:init") 13 | const muted_users = yield MicroBlogApi.get_muted_users(); 14 | if (muted_users && muted_users !== API_ERROR) { 15 | self.muted_users = muted_users; 16 | } 17 | }), 18 | 19 | report_user: flow(function* (username) { 20 | console.log("Reporting:report_user", username) 21 | Alert.alert( 22 | `Report @${username}?`, 23 | `Report @${username} to Micro.blog for review? We'll look at this user's posts to determine if they violate our community guidelines.`, 24 | [ 25 | { 26 | text: "Cancel", 27 | style: 'cancel', 28 | }, 29 | { 30 | text: "Report", 31 | onPress: () => Reporting.handle_report_user(username), 32 | style: 'destructive' 33 | }, 34 | ], 35 | {cancelable: false}, 36 | ); 37 | }), 38 | 39 | handle_report_user: flow(function* (username) { 40 | console.log("Reporting:handle_report_user", username) 41 | self.is_sending_report = true; 42 | const report = yield MicroBlogApi.report_user(username) 43 | if (report !== REPORTING_ERROR) { 44 | Toast.showWithGravity(`@${ username } has been reported.`, Toast.SHORT, Toast.CENTER) 45 | } 46 | else { 47 | alert("Something went wrong. Please try again.") 48 | } 49 | self.is_sending_report = false; 50 | }) 51 | 52 | })) 53 | .views(self => ({ 54 | 55 | })) 56 | .create() 57 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/UIBarButtonItem+Plainify.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+Plainify.m 3 | // MicroBlog_RN 4 | // 5 | // Created by Manton Reece on 8/30/25. 6 | // 7 | 8 | #import "UIBarButtonItem+Plainify.h" 9 | 10 | @import ObjectiveC; 11 | 12 | @implementation UIBarButtonItem (Plainify) 13 | 14 | + (void) load 15 | { 16 | static dispatch_once_t once_token; 17 | dispatch_once(&once_token, ^{ 18 | Class cls = self; 19 | 20 | Method origInit = class_getInstanceMethod(cls, @selector(initWithCustomView:)); 21 | Method swizInit = class_getInstanceMethod(cls, @selector(mb_initWithCustomView:)); 22 | if (origInit && swizInit) { 23 | method_exchangeImplementations(origInit, swizInit); 24 | } 25 | }); 26 | } 27 | 28 | - (instancetype) mb_initWithCustomView:(UIView *)customView 29 | { 30 | // call original (now-swizzled) init 31 | UIBarButtonItem *item = [self mb_initWithCustomView:customView]; 32 | 33 | if (@available(iOS 26, *)) { 34 | UIActivityIndicatorView* spinner = [self findFirstIndicatorInView:customView]; 35 | if (spinner) { 36 | [item performSelector:@selector(setHidesSharedBackground:) withObject:@(YES)]; 37 | } 38 | } 39 | 40 | return item; 41 | } 42 | 43 | - (UIActivityIndicatorView *) findFirstIndicatorInView:(UIView *)rootView 44 | { 45 | NSMutableArray* queue = [NSMutableArray arrayWithObject:rootView]; 46 | 47 | while (queue.count > 0) { 48 | UIView* view = [queue firstObject]; 49 | [queue removeObjectAtIndex:0]; 50 | 51 | if ([view isKindOfClass:[UIActivityIndicatorView class]]) { 52 | return (UIActivityIndicatorView *)view; 53 | } 54 | 55 | // add subviews to the queue 56 | for (UIView *subview in view.subviews) { 57 | [queue addObject:subview]; 58 | } 59 | } 60 | 61 | return nil; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /ios/MicroBlog_Share/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HostAppBundleIdentifier 6 | blog.micro.ios 7 | HostAppURLScheme 8 | microblog:// 9 | NSExtension 10 | 11 | NSAppTransportSecurity 12 | 13 | NSAllowsArbitraryLoads 14 | 15 | NSExceptionDomains 16 | 17 | localhost 18 | 19 | NSExceptionAllowsInsecureHTTPLoads 20 | 21 | 22 | 23 | 24 | NSExtensionAttributes 25 | 26 | NSExtensionActivationRule 27 | 28 | NSExtensionActivationSupportsImageWithMaxCount 29 | 1 30 | NSExtensionActivationSupportsText 31 | 32 | NSExtensionActivationSupportsWebPageWithMaxCount 33 | 1 34 | NSExtensionActivationSupportsWebURLWithMaxCount 35 | 1 36 | 37 | NSExtensionJavaScriptPreprocessingFile 38 | GetURL 39 | 40 | NSExtensionMainStoryboard 41 | MainInterface 42 | NSExtensionPointIdentifier 43 | com.apple.share-services 44 | ReactShareViewBackgroundColor 45 | 46 | Alpha 47 | 1 48 | Blue 49 | 1 50 | Green 51 | 1 52 | Red 53 | 1 54 | Transparent 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/stores/models/posting/Page.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | import { DOMParser } from "@xmldom/xmldom"; 3 | 4 | let html_parser = new DOMParser({ onError: (error) => { 5 | // silently ignore errors 6 | }}); 7 | 8 | export default Page = types.model('Page', { 9 | uid: types.identifierNumber, 10 | name: types.maybe(types.string), 11 | content: types.maybe(types.string), 12 | published: types.maybe(types.string), 13 | url: types.maybe(types.string), 14 | template: types.optional(types.boolean, false) 15 | }) 16 | .views(self => ({ 17 | 18 | plain_text_content(){ 19 | let html = "" + self.content + ""; 20 | var text = ""; 21 | try { 22 | let doc = html_parser.parseFromString(html, "text/html"); 23 | text = doc.documentElement.textContent; 24 | } 25 | catch (error) { 26 | // if parse error, just show HTML 27 | text = self.content; 28 | } 29 | 30 | if (text.length > 300) { 31 | text = text.substring(0, 300) + '...' 32 | } 33 | 34 | return text.trim(); 35 | }, 36 | 37 | images_from_content(){ 38 | if (!self.content) { 39 | return [] 40 | } 41 | 42 | const img_regex = //g 43 | const video_regex = //g 44 | const images = [] 45 | 46 | let match 47 | while ((match = img_regex.exec(self.content)) !== null) { 48 | images.push(match[1]) 49 | } 50 | 51 | while ((match = video_regex.exec(self.content)) !== null) { 52 | const poster = match[1] ? match[1].trim() : "" 53 | if (poster.length > 0) { 54 | images.push(poster) 55 | } 56 | } 57 | 58 | return images; 59 | }, 60 | 61 | nice_local_published_date(){ 62 | const date = new Date(self.published); 63 | return date.toLocaleString(); 64 | }, 65 | 66 | })) 67 | -------------------------------------------------------------------------------- /src/components/tabs/tab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform, Image } from 'react-native'; 4 | import { SFSymbol } from "react-native-sfsymbols"; 5 | import App from '../../stores/App'; 6 | import TimelineIcon from './../../assets/icons/tab_bar/timeline.png'; 7 | import MentionsIcon from './../../assets/icons/tab_bar/mentions.png'; 8 | import DiscoverIcon from './../../assets/icons/tab_bar/discover.png'; 9 | import BookmarksIcon from './../../assets/icons/nav/bookmarks.png'; 10 | 11 | @observer 12 | export default class Tab extends React.Component { 13 | 14 | _returnIconNameOrAsset() { 15 | const { route } = this.props; 16 | const isIOS = Platform.OS === "ios"; 17 | 18 | switch (route.name) { 19 | case "TimelineStack": 20 | return isIOS ? "bubble.left.and.bubble.right" : TimelineIcon; 21 | case "MentionsStack": 22 | return isIOS ? "at" : MentionsIcon; 23 | case "DiscoverStack": 24 | return isIOS ? "magnifyingglass" : DiscoverIcon; 25 | case "BookmarksStack": 26 | return isIOS ? "star" : BookmarksIcon; 27 | default: 28 | return isIOS ? "questionmark" : null; 29 | } 30 | } 31 | 32 | render() { 33 | const iconNameOrAsset = this._returnIconNameOrAsset(); 34 | const { focused } = this.props; 35 | const color = focused ? App.theme_accent_color() : App.theme_text_color(); 36 | 37 | if (Platform.OS === "ios") { 38 | return ( 39 | 45 | ); 46 | } else { 47 | return iconNameOrAsset ? ( 48 | 52 | ) : null; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/header/profile_image.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TouchableOpacity, View, Image } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import App from '../../stores/App'; 6 | import { isLiquidGlass } from './../../utils/ui'; 7 | 8 | @observer 9 | export default class ProfileImage extends React.Component{ 10 | 11 | render() { 12 | const routeKey = this.props.routeKey || 'default' 13 | let button_style = { 14 | width: 28, 15 | height: 28, 16 | marginRight: 12 17 | }; 18 | 19 | if (isLiquidGlass()) { 20 | button_style = { 21 | paddingLeft: 4, 22 | paddingTop: 2 23 | } 24 | } 25 | 26 | if(Auth.selected_user != null){ 27 | return( 28 | { App.open_sheet("main_sheet"); Auth.selected_user.check_token_validity()} } 31 | onLongPress={() => App.navigate_to_screen("user", Auth.selected_user.username)} 32 | accessibilityRole="button" 33 | accessibilityLabel={`Profile menu for ${Auth.selected_user.username}`} 34 | > 35 | { 36 | Auth.selected_user.avatar != null && Auth.selected_user.avatar !== "" ? 37 | 45 | : 46 | 47 | } 48 | 49 | 50 | ) 51 | } 52 | return 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/components/sheets/notifications.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; 4 | import App from '../../stores/App'; 5 | import PushNotifications from "../push/push_notifications"; 6 | import ActionSheet from "react-native-actions-sheet"; 7 | import Push from '../../stores/Push'; 8 | 9 | @observer 10 | export default class NotificationsSheetMenu extends React.Component{ 11 | 12 | render() { 13 | return( 14 | Push.toggle_notifications_open(true)} 23 | onClose={() => Push.toggle_notifications_open(false)} 24 | containerStyle={{ 25 | backgroundColor: App.theme_background_color_secondary(), 26 | padding: 15, 27 | borderRadius: 16 28 | }} 29 | > 30 | 31 | Notifications 32 | 33 | Clear all 34 | 35 | 36 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/components/sheets/menu.js: -------------------------------------------------------------------------------- 1 | import ActionSheet from "react-native-actions-sheet"; 2 | import * as React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | import { View, Text, TouchableOpacity, Platform } from 'react-native'; 5 | import Auth from './../../stores/Auth'; 6 | import AccountSwitcher from '../menu/account_switcher' 7 | import MenuNavigation from '../menu/nav' 8 | import App from '../../stores/App'; 9 | 10 | @observer 11 | export default class SheetMenu extends React.Component{ 12 | 13 | render() { 14 | return( 15 | 26 | 35 | 36 | { 37 | Auth.selected_user != null ? 38 | 39 | : 40 | 49 | Please sign in to continue 50 | 51 | } 52 | 53 | 54 | 55 | ) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /ios/MicroBlog_Share/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/stores/Replies.js: -------------------------------------------------------------------------------- 1 | import { types, flow, destroy } from 'mobx-state-tree'; 2 | import MicroBlogApi, { API_ERROR, DELETE_ERROR } from '../api/MicroBlogApi'; 3 | import Reply from './models/Reply' 4 | import App from './App' 5 | import { Alert } from 'react-native'; 6 | import Auth from './Auth'; 7 | 8 | export default Replies = types.model('Replies', { 9 | current_username: types.maybeNull(types.string), 10 | replies: types.optional(types.array(Reply), []), 11 | is_loading: types.optional(types.boolean, false), 12 | selected_reply: types.maybeNull(types.reference(Reply)) 13 | }) 14 | .actions(self => ({ 15 | 16 | hydrate: flow(function* () { 17 | console.log("Replies:hydrate") 18 | if(self.current_username == null || self.current_username !== Auth.selected_user?.username){ 19 | self.replies = [] 20 | self.current_username = Auth.selected_user?.username 21 | } 22 | self.selected_reply = null 23 | self.is_loading = true 24 | const replies = yield MicroBlogApi.get_replies() 25 | if(replies !== API_ERROR && replies.items != null){ 26 | self.replies = replies.items 27 | } 28 | self.is_loading = false 29 | }), 30 | 31 | refresh: flow(function* () { 32 | self.hydrate() 33 | }), 34 | 35 | select_reply_and_open_edit: flow(function* (reply) { 36 | console.log("Reply:select_reply", reply) 37 | self.selected_reply = reply 38 | App.navigate_to_screen("reply_edit") 39 | }), 40 | 41 | delete_reply: flow(function* (reply) { 42 | console.log("Reply:delete_reply", reply) 43 | const reply_id = reply.id 44 | destroy(reply) 45 | const status = yield MicroBlogApi.delete_post(reply_id) 46 | if(status !== DELETE_ERROR){ 47 | App.show_toast("Reply was deleted.") 48 | self.hydrate() 49 | } 50 | else{ 51 | Alert.alert("Whoops", "Could not delete reply. Please try again.") 52 | self.hydrate() 53 | self.is_loading = false 54 | } 55 | }), 56 | 57 | })) 58 | .create({}) -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Use this property to specify which architecture you want to build. 28 | # You can also override it from the CLI using 29 | # ./gradlew -PreactNativeArchitectures=x86_64 30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 31 | 32 | # Use this property to enable support to the new architecture. 33 | # This will allow you to use TurboModules and the Fabric render in 34 | # your application. You should enable this flag either if you want 35 | # to write custom TurboModules/Fabric components OR use libraries that 36 | # are providing them. 37 | newArchEnabled=true 38 | 39 | # Use this property to enable or disable the Hermes JS engine. 40 | # If set to false, you will be using JSC instead. 41 | hermesEnabled=true 42 | -------------------------------------------------------------------------------- /src/components/header/new_upload.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { Image, Platform } from 'react-native' 4 | import Auth from './../../stores/Auth' 5 | import App from '../../stores/App' 6 | import { SFSymbol } from "react-native-sfsymbols" 7 | import { MenuView } from '@react-native-menu/menu'; 8 | import AddIcon from './../../assets/icons/add.png' 9 | 10 | @observer 11 | export default class NewUploadButton extends React.Component { 12 | 13 | render() { 14 | if (Auth.selected_user != null && Auth.selected_user.posting?.posting_enabled()) { 15 | const { config } = Auth.selected_user.posting.selected_service 16 | return ( 17 | { 19 | const event_id = nativeEvent.event 20 | if (event_id === 'upload_media') { 21 | console.log('upload_media') 22 | Auth.selected_user.posting.selected_service?.pick_image(config?.temporary_destination()) 23 | } else if (event_id === 'upload_file') { 24 | console.log('upload_file') 25 | Auth.selected_user.posting.selected_service?.pick_file(config?.temporary_destination()) 26 | } 27 | }} 28 | actions={[ 29 | { 30 | title: "Photo library", 31 | id: "upload_media", 32 | image: Platform.select({ 33 | ios: 'photo' 34 | }) 35 | }, 36 | { 37 | title: "Files", 38 | id: "upload_file", 39 | image: Platform.select({ 40 | ios: 'folder' 41 | }) 42 | } 43 | ]} 44 | > 45 | { 46 | Platform.OS === 'ios' ? 47 | 52 | : 53 | 57 | } 58 | 59 | ) 60 | } 61 | return null 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/components/search_bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Text, View, TextInput, TouchableOpacity } from 'react-native'; 4 | import App from '../stores/App'; 5 | 6 | @observer 7 | export default class SearchBar extends React.Component { 8 | render() { 9 | const { 10 | placeholder, 11 | onSubmitEditing, 12 | onChangeText, 13 | value, 14 | onCancel 15 | } = this.props; 16 | 17 | return ( 18 | 28 | 54 | 64 | 69 | Cancel 70 | 71 | 72 | 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/stores/models/posting/Post.js: -------------------------------------------------------------------------------- 1 | import { types } from 'mobx-state-tree'; 2 | import { DOMParser } from "@xmldom/xmldom"; 3 | 4 | let html_parser = new DOMParser({ onError: (error) => { 5 | // silently ignore errors 6 | }}); 7 | 8 | export default Post = types.model('Post', { 9 | uid: types.identifierNumber, 10 | name: types.maybe(types.string), 11 | content: types.maybe(types.string), 12 | published: types.maybe(types.string), 13 | url: types.maybe(types.string), 14 | post_status: types.maybe(types.string), 15 | category: types.optional(types.array(types.string), []), 16 | summary: types.maybeNull(types.string) 17 | }) 18 | .views(self => ({ 19 | 20 | plain_text_content(){ 21 | let html = "" + self.content + ""; 22 | var text = ""; 23 | try { 24 | let doc = html_parser.parseFromString(html, "text/html"); 25 | text = doc.documentElement.textContent; 26 | } 27 | catch (error) { 28 | // if parse error, just show HTML 29 | text = self.content; 30 | } 31 | 32 | if (text.length > 300) { 33 | text = text.substring(0, 300) + '...' 34 | } 35 | 36 | return text.trim(); 37 | }, 38 | 39 | images_from_content(){ 40 | if (!self.content) { 41 | return [] 42 | } 43 | 44 | const img_regex = //g 45 | const video_regex = //g 46 | const images = [] 47 | 48 | let match 49 | while ((match = img_regex.exec(self.content)) !== null) { 50 | images.push(match[1]) 51 | } 52 | 53 | while ((match = video_regex.exec(self.content)) !== null) { 54 | const poster = match[1] ? match[1].trim() : "" 55 | if (poster.length > 0) { 56 | images.push(poster) 57 | } 58 | } 59 | 60 | return images; 61 | }, 62 | 63 | nice_local_published_date(){ 64 | const date = new Date(self.published); 65 | return date.toLocaleString(); 66 | }, 67 | 68 | is_draft() { 69 | return self.post_status == "draft" 70 | } 71 | 72 | })) 73 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | # Resolve react_native_pods.rb with node to allow for hoisting 3 | require Pod::Executable.execute_command('node', ['-p', 4 | 'require.resolve( 5 | "react-native/scripts/react_native_pods.rb", 6 | {paths: [process.argv[1]]}, 7 | )', __dir__]).strip 8 | 9 | platform :ios, min_ios_version_supported 10 | prepare_react_native_project! 11 | 12 | linkage = ENV['USE_FRAMEWORKS'] 13 | if linkage != nil 14 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 15 | use_frameworks! :linkage => linkage.to_sym 16 | end 17 | 18 | target 'MicroBlog_RN' do 19 | use_expo_modules! 20 | 21 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' 22 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; 23 | else 24 | config_command = [ 25 | 'node', 26 | '--no-warnings', 27 | '--eval', 28 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 29 | 'react-native-config', 30 | '--json', 31 | '--platform', 32 | 'ios' 33 | ] 34 | end 35 | 36 | config = use_native_modules!(config_command) 37 | 38 | use_react_native!( 39 | :path => config[:reactNativePath], 40 | # An absolute path to your application root. 41 | :app_path => "#{Pod::Config.instance.installation_root}/.." 42 | ) 43 | 44 | target 'MicroBlog_Share' do 45 | inherit! :complete 46 | end 47 | 48 | post_install do |installer| 49 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 50 | react_native_post_install( 51 | installer, 52 | config[:reactNativePath], 53 | :mac_catalyst_enabled => false 54 | ) 55 | 56 | installer.pods_project.targets.each do |target| 57 | target.build_configurations.each do |config| 58 | config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/screens/stacks/DiscoverStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import DiscoverScreen from '../discover/discover'; 6 | import ProfileImage from './../../components/header/profile_image'; 7 | import NewPostButton from '../../components/header/new_post'; 8 | import BackButton from '../../components/header/back'; 9 | import DiscoverTopicScreen from '../../screens/discover/topic'; 10 | import { getSharedScreens } from './SharedStack' 11 | import App from '../../stores/App' 12 | 13 | const DiscoverStack = createNativeStackNavigator(); 14 | 15 | @observer 16 | export default class Discover extends React.Component{ 17 | 18 | render() { 19 | const sharedScreens = getSharedScreens(DiscoverStack, "Discover") 20 | return( 21 | 28 | ({ 32 | headerLeft: () => , 33 | headerRight: () => 34 | })} 35 | /> 36 | ({ 38 | headerLeft: () => , 39 | headerBackTitleVisible: false 40 | })} 41 | > 42 | {sharedScreens} 43 | ({ 47 | headerTitle: `${route.params?.topic.emoji} ${route.params?.topic.title}`, 48 | headerRight: () => 49 | })} 50 | /> 51 | 52 | 53 | ) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/components/sheets/tagmoji.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity } from 'react-native'; 4 | import { ScrollView } from 'react-native-gesture-handler'; 5 | import ActionSheet from "react-native-actions-sheet"; 6 | import Discover from '../../stores/Discover' 7 | import App from '../../stores/App' 8 | import SheetHeader from "./header"; 9 | 10 | @observer 11 | export default class TagmojiMenu extends React.Component{ 12 | 13 | _return_tagmoji_menu() { 14 | return ( 15 | 23 | { 24 | Discover.tagmoji.map((tagmoji, index) => { 25 | return ( 26 | { App.close_sheet("tagmoji_menu"); App.navigate_to_screen("discover/topic", tagmoji) }} 35 | > 36 | {tagmoji.emoji} 37 | {tagmoji.name} 38 | 39 | ) 40 | } 41 | ) 42 | } 43 | 44 | ) 45 | } 46 | 47 | render() { 48 | return( 49 | 60 | 61 | 67 | {this._return_tagmoji_menu()} 68 | 69 | 70 | ) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/stores/models/Notification.js: -------------------------------------------------------------------------------- 1 | import { types, flow, destroy } from 'mobx-state-tree'; 2 | import MicroBlogApi from '../../api/MicroBlogApi' 3 | import App from '../App' 4 | import Auth from '../Auth' 5 | import Push from '../Push' 6 | 7 | export default Notification = types.model('Notification', { 8 | id: types.identifier, 9 | post_id: types.maybeNull(types.string), 10 | message: types.maybeNull(types.string), 11 | from_username: types.maybeNull(types.string), 12 | from_avatar_url: types.maybeNull(types.string), 13 | to_username: types.maybeNull(types.string), 14 | should_open: types.maybeNull(types.boolean), 15 | did_load_before_user_was_loaded: types.maybeNull(types.boolean) 16 | }) 17 | .actions(self => ({ 18 | 19 | hydrate: flow(function* () { 20 | console.log("Notification:hydrate") 21 | if (self.from_username) { 22 | const profile = yield MicroBlogApi.get_profile(self.from_username) 23 | if (profile) { 24 | console.log("Notification:hydrate:profile", profile.author?.avatar) 25 | self.from_avatar_url = profile.author?.avatar 26 | } 27 | } 28 | }), 29 | 30 | afterCreate: flow(function* () { 31 | if (self.should_open != null && self.should_open) { 32 | self.handle_action() 33 | } 34 | else{ 35 | self.hydrate() 36 | } 37 | }), 38 | 39 | handle_action: flow(function* () { 40 | console.log("Notification:handle_action", self, self.local_user()) 41 | if (self.local_user() != null) { 42 | if (Auth.selected_user !== self.local_user()) { 43 | yield Auth.select_user(self.local_user()) 44 | } 45 | App.navigate_to_screen("open", self.post_id) 46 | Push.close_notification_sheet() 47 | } 48 | }), 49 | 50 | remove: flow(function* () { 51 | destroy(self) 52 | }) 53 | 54 | })) 55 | .views(self => ({ 56 | 57 | local_user() { 58 | return Auth.users.find(u => u.username === self.to_username) 59 | }, 60 | 61 | can_show_notification() { 62 | return self.message && this.local_user() != null && !self.should_open 63 | }, 64 | 65 | trimmed_message() { 66 | if (self.message.length > 255) { 67 | return `${self.message.slice(0, 250)}...` 68 | } 69 | return self.message 70 | } 71 | 72 | })) 73 | -------------------------------------------------------------------------------- /src/screens/replies/edit.android.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TextInput } from 'react-native'; 4 | import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; 5 | import ReplyToolbar from '../../components/keyboard/reply_toolbar' 6 | import App from '../../stores/App' 7 | import Replies from '../../stores/Replies' 8 | import LoadingComponent from '../../components/generic/loading'; 9 | 10 | @observer 11 | export default class ReplyEditScreen extends React.Component{ 12 | 13 | render() { 14 | return( 15 | <> 16 | 17 | 0 ? 90 : 50, 25 | }} 26 | editable={!Replies.selected_reply?.is_sending_reply} 27 | multiline={true} 28 | scrollEnabled={true} 29 | returnKeyType={'default'} 30 | keyboardType={'default'} 31 | autoFocus={true} 32 | autoCorrect={true} 33 | clearButtonMode={'while-editing'} 34 | enablesReturnKeyAutomatically={true} 35 | underlineColorAndroid={'transparent'} 36 | value={Replies.selected_reply?.reply_text} 37 | onChangeText={(text) => !Replies.selected_reply?.is_sending_reply ? Replies.selected_reply.set_reply_text(text) : null} 38 | onSelectionChange={({ nativeEvent: { selection } }) => { 39 | Replies.selected_reply?.set_text_selection(selection) 40 | }} 41 | /> 42 | 43 | 44 | 45 | 46 | 47 | > 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/screens/replies/replies.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, ScrollView, RefreshControl } from 'react-native'; 4 | import Auth from './../../stores/Auth'; 5 | import LoginMessage from '../../components/info/login_message' 6 | import App from '../../stores/App' 7 | import Replies from '../../stores/Replies'; 8 | import ReplyCell from '../../components/cells/reply_cell'; 9 | 10 | @observer 11 | export default class RepliesScreen extends React.Component{ 12 | 13 | componentDidMount() { 14 | Replies.hydrate() 15 | } 16 | 17 | _return_header = () => { 18 | return( 19 | 30 | Replies can be edited in the first 24 hours after posting. 31 | 32 | ) 33 | } 34 | 35 | _return_replies_list = () => { 36 | return( 37 | 48 | } 49 | > 50 | { 51 | Replies.replies.map((reply) => { 52 | return 53 | }) 54 | } 55 | 56 | ) 57 | } 58 | 59 | render() { 60 | return( 61 | 62 | { 63 | Auth.is_logged_in() && !Auth.is_selecting_user ? 64 | <> 65 | {this._return_header()} 66 | {this._return_replies_list()} 67 | > 68 | : 69 | 70 | } 71 | 72 | ) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/components/settings/user_muting.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { View, Text, TouchableOpacity, Platform } from "react-native"; 4 | import { SFSymbol } from "react-native-sfsymbols"; 5 | import { SvgXml } from "react-native-svg"; 6 | import App from "../../stores/App"; 7 | import Auth from "../../stores/Auth"; 8 | import MBImage from "../common/MBImage"; 9 | 10 | @observer 11 | export default class UserMutingSettings extends React.Component { 12 | render() { 13 | const { user, index } = this.props; 14 | return ( 15 | App.navigate_to_screen("muting", user)} 17 | style={{ 18 | width: "100%", 19 | flexDirection: "row", 20 | justifyContent: "space-between", 21 | alignItems: "center", 22 | paddingVertical: 10, 23 | borderBottomWidth: Auth.users.length - 1 !== index ? 1 : 0, 24 | borderColor: App.theme_border_color(), 25 | }} 26 | > 27 | 28 | 33 | 34 | @{user.username} 35 | 36 | 37 | 38 | {Platform.OS === "ios" ? ( 39 | 44 | ) : ( 45 | 50 | )} 51 | 52 | 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/settings/user_posting.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { View, Text, TouchableOpacity } from "react-native"; 4 | import App from "../../stores/App"; 5 | import Auth from "../../stores/Auth"; 6 | import { SvgXml } from "react-native-svg"; 7 | import MBImage from "../common/MBImage"; 8 | 9 | @observer 10 | export default class UserPostingSettings extends React.Component { 11 | render() { 12 | const { user, index } = this.props; 13 | return ( 14 | 23 | 30 | 31 | 41 | 42 | App.navigate_to_screen("post_service", user)} 44 | style={{ flexDirection: "row", alignItems: "center" }} 45 | > 46 | 47 | {user.posting?.selected_service?.description()} 48 | 49 | 52 | 53 | 54 | `} 55 | /> 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/screens/replies/edit.ios.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { TextInput } from 'react-native'; 4 | import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; 5 | import ReplyToolbar from '../../components/keyboard/reply_toolbar' 6 | import App from '../../stores/App' 7 | import Replies from '../../stores/Replies' 8 | import LoadingComponent from '../../components/generic/loading'; 9 | 10 | @observer 11 | export default class ReplyEditScreen extends React.Component{ 12 | 13 | render() { 14 | return( 15 | <> 16 | 17 | 280 ? 150 : 0, 28 | }} 29 | editable={!Replies.selected_reply?.is_sending_reply} 30 | multiline={true} 31 | scrollEnabled={true} 32 | returnKeyType={'default'} 33 | keyboardType={'default'} 34 | autoFocus={true} 35 | autoCorrect={true} 36 | clearButtonMode={'while-editing'} 37 | enablesReturnKeyAutomatically={true} 38 | underlineColorAndroid={'transparent'} 39 | value={Replies.selected_reply?.reply_text} 40 | onChangeText={(text) => !Replies.selected_reply?.is_sending_reply ? Replies.selected_reply.set_reply_text(text) : null} 41 | onSelectionChange={({ nativeEvent: { selection } }) => { 42 | Replies.selected_reply?.set_text_selection(selection) 43 | }} 44 | /> 45 | 46 | 47 | 48 | 49 | 50 | > 51 | ) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /android/app/src/main/java/blog/micro/android/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package blog.micro.android; 2 | import android.content.res.Configuration 3 | import expo.modules.ApplicationLifecycleDispatcher 4 | import expo.modules.ReactNativeHostWrapper 5 | 6 | import android.app.Application 7 | import com.facebook.react.PackageList 8 | import com.facebook.react.ReactApplication 9 | import com.facebook.react.ReactHost 10 | import com.facebook.react.ReactNativeHost 11 | import com.facebook.react.ReactPackage 12 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 13 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 14 | import com.facebook.react.defaults.DefaultReactNativeHost 15 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 16 | import com.facebook.soloader.SoLoader 17 | 18 | class MainApplication : Application(), ReactApplication { 19 | 20 | override val reactNativeHost: ReactNativeHost = 21 | ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { 22 | override fun getPackages(): List = 23 | PackageList(this).packages.apply { 24 | // Packages that cannot be autolinked yet can be added manually here, for example: 25 | // add(MyReactNativePackage()) 26 | } 27 | 28 | override fun getJSMainModuleName(): String = "index" 29 | 30 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 31 | 32 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 33 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 34 | }) 35 | 36 | override val reactHost: ReactHost 37 | get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) 38 | 39 | override fun onCreate() { 40 | super.onCreate() 41 | SoLoader.init(this, OpenSourceMergedSoMapping) 42 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 43 | // If you opted-in for the New Architecture, we load the native entry point for this app. 44 | load() 45 | } 46 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 47 | } 48 | 49 | override fun onConfigurationChanged(newConfig: Configuration) { 50 | super.onConfigurationChanged(newConfig) 51 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/header/back.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import { isLiquidGlass, STANDARD_SLOP } from './../../utils/ui'; 6 | import { SFSymbol } from 'react-native-sfsymbols'; 7 | import { SvgXml } from 'react-native-svg'; 8 | import { HeaderBackButton } from '@react-navigation/elements'; 9 | import { useNavigation } from '@react-navigation/native'; 10 | 11 | const BackButtonContent = observer(() => { 12 | const navigation = useNavigation(); 13 | let button_style = {}; 14 | 15 | if (isLiquidGlass()) { 16 | button_style = { 17 | marginRight: 0 - STANDARD_SLOP, 18 | marginLeft: 16 - STANDARD_SLOP, 19 | marginTop: 2 - STANDARD_SLOP, 20 | marginBottom: 0 - STANDARD_SLOP, 21 | padding: STANDARD_SLOP 22 | } 23 | } 24 | else { 25 | button_style = { 26 | marginLeft: -20, 27 | marginRight: 0, 28 | marginTop: -10, 29 | marginBottom: -10, 30 | paddingLeft: 20, 31 | paddingRight: 0, 32 | paddingTop: 10, 33 | paddingBottom: 10 34 | }; 35 | } 36 | 37 | return ( 38 | { 40 | if (navigation.canGoBack()) { 41 | navigation.goBack() 42 | } 43 | }} 44 | style={button_style} 45 | labelVisible={false} 46 | hitSlop={{ top: 5, bottom: 5, left: 5, right: 5 }} 47 | backImage={() => ( 48 | Platform.OS === 'ios' ? 49 | 54 | : 55 | 67 | )} 68 | /> 69 | ) 70 | }) 71 | 72 | @observer 73 | export default class BackButton extends React.Component { 74 | render() { 75 | return 76 | } 77 | } -------------------------------------------------------------------------------- /src/screens/stacks/BookmarksStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Platform, View } from 'react-native'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import BookmarksScreen from '../bookmarks/bookmarks'; 6 | import ProfileImage from './../../components/header/profile_image'; 7 | import AddBookmarkButton from '../../components/header/add_bookmark'; 8 | import BookmarkScreen from '../bookmarks/bookmark'; 9 | import NewPostButton from '../../components/header/new_post'; 10 | import { getSharedScreens } from './SharedStack' 11 | import TagsButton from '../../components/header/tags_button'; 12 | import BackButton from '../../components/header/back'; 13 | import App from '../../stores/App' 14 | 15 | const BookmarksStack = createNativeStackNavigator(); 16 | 17 | @observer 18 | export default class Bookmarks extends React.Component{ 19 | 20 | render() { 21 | const sharedScreens = getSharedScreens(BookmarksStack, "Bookmarks") 22 | return( 23 | 30 | ({ 34 | headerLeft: () => , 35 | headerRight: () => ( 36 | 37 | 38 | 39 | 40 | ) 41 | })} 42 | /> 43 | ({ 45 | headerLeft: () => , 46 | headerBackTitleVisible: false 47 | })} 48 | > 49 | {sharedScreens} 50 | ({ 54 | headerTitle: `Bookmark`, 55 | headerRight: () => 56 | })} 57 | /> 58 | 59 | 60 | ) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/components/bookmarks/tag_filter_header.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity, Platform } from 'react-native'; 4 | import App from './../../stores/App'; 5 | import Auth from '../../stores/Auth'; 6 | import { SvgXml } from 'react-native-svg'; 7 | import { SFSymbol } from "react-native-sfsymbols"; 8 | 9 | @observer 10 | export default class TagFilterHeader extends React.Component{ 11 | 12 | render() { 13 | if(Auth.selected_user?.selected_tag != null){ 14 | return( 15 | 16 | 17 | Auth.selected_user?.set_selected_tag(null)} 31 | > 32 | { 33 | Platform.OS === "ios" ? 34 | 39 | : 40 | 51 | } 52 | 53 | tag: {Auth.selected_user?.selected_tag} 54 | 55 | 56 | ) 57 | } 58 | return null 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/components/keyboard/username_toolbar.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { 4 | View, 5 | Text, 6 | TouchableOpacity, 7 | Image, 8 | Platform, 9 | ScrollView, 10 | } from "react-native"; 11 | import App from "../../stores/App"; 12 | 13 | @observer 14 | export default class UsernameToolbar extends React.Component { 15 | render() { 16 | if (App.found_users.length == 0) { 17 | return null; 18 | } else { 19 | return ( 20 | 30 | 40 | {App.found_users.map((u, index) => { 41 | return ( 42 | { 51 | App.update_autocomplete(u.username, this.props.object); 52 | }} 53 | > 54 | 63 | 70 | @{u.username} 71 | 72 | 73 | ); 74 | })} 75 | 76 | 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/share/header.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { View, Text, TouchableOpacity, Platform } from 'react-native' 4 | import Share from '../../stores/Share' 5 | import App from '../../stores/App' 6 | import { SvgXml } from 'react-native-svg' 7 | 8 | @observer 9 | export default class ShareHeaderComponent extends React.Component { 10 | 11 | render() { 12 | const header_base_style = { 13 | paddingVertical: 15, 14 | paddingHorizontal: 8, 15 | borderBottomWidth: 1, 16 | borderBottomColor: App.theme_border_color(), 17 | flexDirection: 'row', 18 | justifyContent: 'space-between' 19 | } 20 | 21 | let header_ios_style = {}; 22 | if ((Platform.OS == 'ios') && (parseInt(Platform.Version, 10) >= 26)) { 23 | // Liquid Glass 24 | header_ios_style = { 25 | marginTop: 3, 26 | marginLeft: 7, 27 | marginRight: 7 28 | } 29 | } 30 | 31 | return ( 32 | 33 | { 34 | Share.image_options_open ? 35 | 36 | 38 | 39 | `} 40 | width="24" 41 | height="24" 42 | /> 43 | 44 | : 45 | 46 | `} 48 | width="24" 49 | height="24" 50 | /> 51 | 52 | } 53 | { 54 | !Share.image_options_open && 55 | 56 | { 57 | Share.can_save_as_bookmark() && 58 | 59 | Save Bookmark 60 | 61 | } 62 | 63 | Post 64 | 65 | 66 | } 67 | 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/screens/share/post.android.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { View, Text } from 'react-native' 4 | import { TextInput } from 'react-native'; 5 | import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; 6 | import Share from '../../stores/Share' 7 | import App from '../../stores/App' 8 | import Auth from '../../stores/Auth'; 9 | import AssetToolbar from '../../components/keyboard/asset_toolbar' 10 | import PostToolbar from '../../components/keyboard/post_toolbar' 11 | 12 | @observer 13 | export default class SharePostScreen extends React.Component { 14 | 15 | render() { 16 | const { selected_user } = Auth 17 | return ( 18 | Share.is_logged_in() && selected_user?.posting != null ? 19 | <> 20 | 21 | { 22 | Share.error_message != null && 23 | 24 | {Share.error_message} 25 | 26 | } 27 | { 28 | Share.can_save_as_bookmark() && 29 | 30 | 31 | You can save this URL as a bookmark or keep editing to create a new post. 32 | 33 | 34 | } 35 | !selected_user?.posting.is_sending_post ? Share.set_post_text(text) : null} 56 | onSelectionChange={({ nativeEvent: { selection } }) => { 57 | Share.set_text_selection(selection) 58 | }} 59 | /> 60 | 61 | 62 | 63 | 64 | 65 | > 66 | : null 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/screens/stacks/TabNavigator.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 4 | import App from '../../stores/App'; 5 | import TimelineStack from './TimelineStack'; 6 | import MentionsStack from './MentionsStack'; 7 | import BookmarksStack from './BookmarksStack'; 8 | import DiscoverStack from './DiscoverStack'; 9 | import TabIcon from '../../components/tabs/tab'; 10 | 11 | const Tab = createBottomTabNavigator(); 12 | 13 | @observer 14 | export default class TabNavigator extends React.Component { 15 | 16 | render() { 17 | return ( 18 | ({ 22 | tabBarStyle: { 23 | borderTopColor: App.theme_tabbar_divider_color(), 24 | borderTopWidth: 0.5, 25 | backgroundColor: App.theme_navbar_background_color(), 26 | elevation: 0, 27 | shadowOpacity: 0 28 | }, 29 | tabBarIcon: ({ focused, color, size }) => { 30 | return ; 31 | }, 32 | headerShown: false, 33 | tabBarActiveTintColor: App.theme_accent_color() 34 | })} 35 | screenListeners={{ 36 | state: (e) => { 37 | App.set_current_tab_index(e.data.state.index) 38 | }, 39 | focus: (e) => { 40 | App.set_current_tab_key(e.target) 41 | }, 42 | tabPress: (e) => { 43 | App.scroll_web_view_to_top(e.target) 44 | } 45 | }} 46 | > 47 | 55 | 63 | 71 | 79 | 80 | ) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/screens/share/index.android.js: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react' 2 | import * as React from 'react' 3 | import { ActivityIndicator, Button, Text, View, Platform } from 'react-native' 4 | import App from '../../stores/App' 5 | import Share from '../../stores/Share' 6 | import Auth from '../../stores/Auth'; 7 | import SharePostScreen from './post' 8 | import ShareImageOptionsScreen from './image_options' 9 | import ShareHeaderComponent from '../../components/share/header' 10 | 11 | @observer 12 | export default class ShareScreen extends React.Component { 13 | 14 | render() { 15 | return ( 16 | 20 | { 21 | Share.is_loading ? 22 | 23 | 24 | 25 | : 26 | Share.is_logged_in() ? 27 | 28 | 29 | { 30 | Share.image_options_open ? 31 | 32 | : 33 | <> 34 | 35 | { 36 | Auth.selected_user?.posting.is_sending_post || Auth.selected_user?.posting.is_adding_bookmark ? 37 | 48 | 53 | 54 | { Auth.selected_user?.posting.is_sending_post ? "Sending post..." : "Saving bookmark..." } 55 | 56 | 57 | : null 58 | } 59 | > 60 | } 61 | 62 | : 63 | 64 | Using the Micro.blog app, please sign in before using the share extension. 65 | 66 | 67 | } 68 | 69 | ) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /ios/MicroBlog_RN/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Micro.blog 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLSchemes 29 | 30 | microblog 31 | 32 | 33 | 34 | CFBundleVersion 35 | $(CURRENT_PROJECT_VERSION) 36 | ITSAppUsesNonExemptEncryption 37 | 38 | LSRequiresIPhoneOS 39 | 40 | NSAppTransportSecurity 41 | 42 | NSExceptionDomains 43 | 44 | localhost 45 | 46 | NSExceptionAllowsInsecureHTTPLoads 47 | 48 | 49 | 50 | 51 | NSCameraUsageDescription 52 | For taking new photos and posting them. 53 | NSFaceIDUsageDescription 54 | Allow $(PRODUCT_NAME) to access your Face ID biometric data. 55 | NSLocationWhenInUseUsageDescription 56 | The location may be used when posting photos. 57 | NSPhotoLibraryUsageDescription 58 | Micro.blog can upload photos to your microblog. 59 | UIBackgroundModes 60 | 61 | remote-notification 62 | 63 | UILaunchStoryboardName 64 | LaunchScreen 65 | UIRequiredDeviceCapabilities 66 | 67 | arm64 68 | 69 | UISupportedInterfaceOrientations 70 | 71 | UIInterfaceOrientationPortrait 72 | 73 | UISupportedInterfaceOrientations~ipad 74 | 75 | UIInterfaceOrientationLandscapeLeft 76 | UIInterfaceOrientationLandscapeRight 77 | UIInterfaceOrientationPortrait 78 | UIInterfaceOrientationPortraitUpsideDown 79 | 80 | UIViewControllerBasedStatusBarAppearance 81 | 82 | UIDesignRequiresCompatibility 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/screens/share/post.ios.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer } from 'mobx-react' 3 | import { InputAccessoryView, View, Text } from 'react-native' 4 | import Share from '../../stores/Share' 5 | import App from '../../stores/App' 6 | import AssetToolbar from '../../components/keyboard/asset_toolbar' 7 | import PostToolbar from '../../components/keyboard/post_toolbar' 8 | import HighlightingText from '../../components/text/highlighting_text'; 9 | 10 | @observer 11 | export default class SharePostScreen extends React.Component { 12 | 13 | constructor (props) { 14 | super(props) 15 | this.input_accessory_view_id = "input_toolbar" 16 | } 17 | 18 | render() { 19 | const { selected_user } = Share 20 | return ( 21 | Share.is_logged_in() && selected_user?.posting != null ? 22 | 23 | { 24 | Share.error_message != null && 25 | 26 | {Share.error_message} 27 | 28 | } 29 | { 30 | Share.can_save_as_bookmark() && 31 | 32 | 33 | You can save this URL as a bookmark or keep editing to create a new post. 34 | 35 | 36 | } 37 | !selected_user?.posting.is_sending_post ? Share.set_post_text(text) : null} 61 | onSelectionChange={({ nativeEvent: { selection } }) => { 62 | Share.set_text_selection(selection) 63 | }} 64 | inputAccessoryViewID={this.input_accessory_view_id} 65 | /> 66 | 67 | 68 | 69 | 70 | 71 | : null 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/screens/stacks/PostingStack.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { View, Platform } from 'react-native' 3 | import { observer } from 'mobx-react'; 4 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 5 | import PostingScreen from '../../screens/posts/new'; 6 | import PostingOptionsScreen from '../../screens/posts/options'; 7 | import CloseModalButton from '../../components/header/close'; 8 | import PostButton from '../../components/header/post_button'; 9 | import RemoveImageButton from '../../components/header/remove_image'; 10 | import ImageOptionsScreen from '../../screens/posts/image_options'; 11 | import UploadsScreen from '../uploads/uploads' 12 | import NewUploadButton from '../../components/header/new_upload'; 13 | import RefreshActivity from '../../components/header/refresh_activity'; 14 | import App from '../../stores/App' 15 | 16 | const PostingStack = createNativeStackNavigator(); 17 | 18 | @observer 19 | export default class Posting extends React.Component{ 20 | 21 | render() { 22 | return( 23 | 34 | , 40 | headerLeft: () => 41 | }} 42 | /> 43 | 50 | ({ 54 | headerTitle: "Image Options", 55 | headerRight: () => 56 | })} 57 | /> 58 | { 64 | return ( 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | }} 72 | /> 73 | 74 | ) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/stores/Discover.js: -------------------------------------------------------------------------------- 1 | import { types, flow } from 'mobx-state-tree'; 2 | import MicroBlogApi, { API_ERROR } from '../api/MicroBlogApi' 3 | 4 | export default Discover = types.model('Discover', { 5 | tagmoji: types.optional(types.array(types.model('Tagmoji', { 6 | name: types.maybeNull(types.string), 7 | title: types.maybeNull(types.string), 8 | emoji: types.maybeNull(types.string), 9 | is_featured: types.maybeNull(types.boolean) 10 | })), []), 11 | random_tagmoji: types.optional(types.array(types.string), []), 12 | search_shown: types.optional(types.boolean, false), 13 | search_query: types.optional(types.string, ""), 14 | search_trigger: types.optional(types.boolean, false), 15 | did_trigger_search: types.optional(types.boolean, false) 16 | }) 17 | .actions(self => ({ 18 | 19 | init: flow(function* () { 20 | console.log("Discover:init") 21 | const tagmoji = yield MicroBlogApi.get_discover_timeline() 22 | if (tagmoji !== API_ERROR && tagmoji != null && tagmoji.length > 0) { 23 | self.tagmoji = tagmoji 24 | self.random_tagmoji = self.random_emojis() 25 | } 26 | }), 27 | 28 | shuffle_random_emoji: flow(function* () { 29 | if (self.tagmoji.length === 0) { 30 | yield self.init() 31 | } 32 | self.random_tagmoji = self.random_emojis() 33 | }), 34 | 35 | toggle_search_bar: flow(function* () { 36 | self.search_shown = !self.search_shown 37 | if(!self.search_shown){ 38 | self.search_query = "" 39 | self.search_trigger = false 40 | self.did_trigger_search = false 41 | } 42 | }), 43 | 44 | set_search_query: flow(function* (value) { 45 | self.search_query = value 46 | if(value === ""){ 47 | self.search_trigger = false 48 | self.did_trigger_search = false 49 | } 50 | }), 51 | 52 | trigger_search: flow(function* (value = true) { 53 | if(value){ 54 | self.did_trigger_search = true 55 | } 56 | self.search_trigger = value 57 | setTimeout(() =>{ 58 | self.trigger_search(false) 59 | }, 50) 60 | 61 | }) 62 | 63 | })) 64 | .views(self => ({ 65 | 66 | random_emojis() { 67 | const emoji_list = self.tagmoji.map(tagmoji => tagmoji.emoji) 68 | const emoji_list_length = emoji_list.length 69 | const emoji_list_random_indexes = [] 70 | for (let i = 0; i < 3; i++) { 71 | let random_index = Math.floor(Math.random() * emoji_list_length) 72 | while (emoji_list_random_indexes.includes(random_index)) { 73 | random_index = Math.floor(Math.random() * emoji_list_length) 74 | } 75 | emoji_list_random_indexes.push(random_index) 76 | } 77 | return emoji_list.filter((_emoji, index) => emoji_list_random_indexes.includes(index)) 78 | }, 79 | 80 | can_show_search(){ 81 | return self.did_trigger_search && self.search_shown && self.search_query !== "" && self.search_query.length >= 3 82 | }, 83 | 84 | should_load_search(){ 85 | return self.search_trigger 86 | }, 87 | 88 | topic_by_slug(slug){ 89 | return self.tagmoji.find(topic => topic.name === slug) 90 | }, 91 | 92 | sanitised_search_query(){ 93 | return self.search_query.trim().replace(/(%20|\s)/g, "+") 94 | } 95 | 96 | })) 97 | .create() 98 | -------------------------------------------------------------------------------- /src/screens/share/index.js: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react' 2 | import * as React from 'react' 3 | import { ActivityIndicator, Button, Text, View, Platform } from 'react-native' 4 | import App from '../../stores/App' 5 | import Share from '../../stores/Share' 6 | import SharePostScreen from './post' 7 | import ShareImageOptionsScreen from './image_options' 8 | import ShareHeaderComponent from '../../components/share/header' 9 | 10 | @observer 11 | export default class ShareScreen extends React.Component { 12 | 13 | componentDidMount() { 14 | console.log('ShareScreen:componentDidMount') 15 | Platform.OS === "ios" && Share.hydrate() 16 | } 17 | 18 | render() { 19 | return ( 20 | 24 | { 25 | Share.is_loading ? 26 | 27 | 28 | { 29 | Platform.OS === "ios" ? 30 | {Share.temp_direct_shared_data} 31 | : null 32 | } 33 | 34 | : 35 | Share.is_logged_in() ? 36 | 37 | 38 | { 39 | Share.image_options_open ? 40 | 41 | : 42 | <> 43 | 44 | { 45 | Share.selected_user?.posting.is_sending_post || Share.selected_user?.posting.is_adding_bookmark ? 46 | 57 | 62 | 63 | { Share.selected_user?.posting.is_sending_post ? "Sending post..." : "Saving bookmark..." } 64 | 65 | 66 | : null 67 | } 68 | > 69 | } 70 | 71 | : 72 | 73 | Using the Micro.blog app, please sign in before using the share extension. 74 | 75 | 76 | } 77 | 78 | ) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.0.5.1) 9 | concurrent-ruby (~> 1.0, >= 1.0.2) 10 | i18n (>= 1.6, < 2) 11 | minitest (>= 5.1) 12 | tzinfo (~> 2.0) 13 | addressable (2.8.7) 14 | public_suffix (>= 2.0.2, < 7.0) 15 | algoliasearch (1.27.5) 16 | httpclient (~> 2.8, >= 2.8.3) 17 | json (>= 1.5.1) 18 | atomos (0.1.3) 19 | base64 (0.2.0) 20 | claide (1.1.0) 21 | cocoapods (1.14.3) 22 | addressable (~> 2.8) 23 | claide (>= 1.0.2, < 2.0) 24 | cocoapods-core (= 1.14.3) 25 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 26 | cocoapods-downloader (>= 2.1, < 3.0) 27 | cocoapods-plugins (>= 1.0.0, < 2.0) 28 | cocoapods-search (>= 1.0.0, < 2.0) 29 | cocoapods-trunk (>= 1.6.0, < 2.0) 30 | cocoapods-try (>= 1.1.0, < 2.0) 31 | colored2 (~> 3.1) 32 | escape (~> 0.0.4) 33 | fourflusher (>= 2.3.0, < 3.0) 34 | gh_inspector (~> 1.0) 35 | molinillo (~> 0.8.0) 36 | nap (~> 1.0) 37 | ruby-macho (>= 2.3.0, < 3.0) 38 | xcodeproj (>= 1.23.0, < 2.0) 39 | cocoapods-core (1.14.3) 40 | activesupport (>= 5.0, < 8) 41 | addressable (~> 2.8) 42 | algoliasearch (~> 1.0) 43 | concurrent-ruby (~> 1.1) 44 | fuzzy_match (~> 2.0.4) 45 | nap (~> 1.0) 46 | netrc (~> 0.11) 47 | public_suffix (~> 4.0) 48 | typhoeus (~> 1.0) 49 | cocoapods-deintegrate (1.0.5) 50 | cocoapods-downloader (2.1) 51 | cocoapods-plugins (1.0.0) 52 | nap 53 | cocoapods-search (1.0.1) 54 | cocoapods-trunk (1.6.0) 55 | nap (>= 0.8, < 2.0) 56 | netrc (~> 0.11) 57 | cocoapods-try (1.2.0) 58 | colored2 (3.1.2) 59 | concurrent-ruby (1.2.2) 60 | escape (0.0.4) 61 | ethon (0.16.0) 62 | ffi (>= 1.15.0) 63 | ffi (1.17.0) 64 | fourflusher (2.3.1) 65 | fuzzy_match (2.0.4) 66 | gh_inspector (1.1.3) 67 | httpclient (2.8.3) 68 | i18n (1.14.1) 69 | concurrent-ruby (~> 1.0) 70 | json (2.7.2) 71 | minitest (5.18.1) 72 | molinillo (0.8.0) 73 | nanaimo (0.3.0) 74 | nap (1.1.0) 75 | netrc (0.11.0) 76 | nkf (0.2.0) 77 | public_suffix (4.0.7) 78 | rexml (3.2.9) 79 | strscan 80 | ruby-macho (2.5.1) 81 | strscan (3.1.0) 82 | typhoeus (1.4.1) 83 | ethon (>= 0.9.0) 84 | tzinfo (2.0.6) 85 | concurrent-ruby (~> 1.0) 86 | xcodeproj (1.24.0) 87 | CFPropertyList (>= 2.3.3, < 4.0) 88 | atomos (~> 0.1.3) 89 | claide (>= 1.0.2, < 2.0) 90 | colored2 (~> 3.1) 91 | nanaimo (~> 0.3.0) 92 | rexml (~> 3.2.4) 93 | 94 | PLATFORMS 95 | ruby 96 | 97 | DEPENDENCIES 98 | activesupport (>= 6.1.7.5, != 7.1.0) 99 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0) 100 | concurrent-ruby (<= 1.3.4) 101 | xcodeproj (< 1.26.0) 102 | 103 | RUBY VERSION 104 | ruby 3.0.4p208 105 | 106 | BUNDLED WITH 107 | 2.7.2 108 | -------------------------------------------------------------------------------- /src/components/sheets/posts_destination.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TouchableOpacity, Image, ScrollView } from 'react-native'; 4 | import ActionSheet, { SheetManager } from "react-native-actions-sheet"; 5 | import Auth from '../../stores/Auth'; 6 | import App from '../../stores/App' 7 | import CheckmarkIcon from '../../assets/icons/checkmark.png'; 8 | 9 | @observer 10 | export default class PostsDestinationMenu extends React.Component{ 11 | 12 | constructor(props){ 13 | super(props); 14 | this.sheetId = props.sheetId 15 | } 16 | 17 | _render_destinations = () => { 18 | const { selected_service } = Auth.selected_user.posting 19 | const { config } = selected_service 20 | return config.destination.map((destination, index, array) => { 21 | const is_last = index === array.length - 1; 22 | const is_selected_blog = config.posts_destination() === destination 23 | return( 24 | { 27 | selected_service.set_active_destination(destination, this.props.payload?.type); 28 | SheetManager.hide(this.sheetId); 29 | }} 30 | style={{ 31 | flexDirection: "row", 32 | justifyContent: "space-between", 33 | alignItems: "center", 34 | paddingVertical: 15, 35 | borderBottomWidth: is_last ? 0 : 1, 36 | borderColor: App.theme_border_color() 37 | }} 38 | > 39 | 40 | {destination.name} 41 | 42 | { 43 | is_selected_blog ? 44 | 45 | : null 46 | } 47 | 48 | ) 49 | }) 50 | } 51 | 52 | render() { 53 | if (Auth.selected_user == null) { return null; } 54 | return( 55 | 63 | 64 | 73 | Blogs 74 | 75 | {this._render_destinations()} 76 | 77 | 78 | 79 | 80 | ) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/screens/bookmarks/add_bookmark.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { View, Text, TextInput, Button, ActivityIndicator, Keyboard, Platform } from 'react-native'; 4 | import { KeyboardAvoidingView } from "react-native-keyboard-controller"; 5 | import App from '../../stores/App' 6 | 7 | @observer 8 | export default class AddBookmarkScreen extends React.Component{ 9 | 10 | constructor (props) { 11 | super(props) 12 | this.state = { 13 | url: "" 14 | } 15 | this._input_ref = React.createRef() 16 | } 17 | 18 | _dismiss = () => { 19 | Keyboard.dismiss() 20 | App.go_back() 21 | } 22 | 23 | _add_bookmark = async () => { 24 | const bookmark = await Auth.selected_user.posting.add_bookmark(this.state.url) 25 | console.log("AddBookmarkScreen:_add_bookmark", bookmark) 26 | if (bookmark) { 27 | this.setState({ url: "" }) 28 | this._input_ref.current.clear() 29 | this._dismiss() 30 | } 31 | } 32 | 33 | render() { 34 | const { posting } = Auth.selected_user 35 | return( 36 | 37 | 38 | 39 | For Micro.blog Premium subscribers, bookmarked web pages are also archived so you can read them later and make highlights. 40 | 41 | !posting.is_adding_bookmark ? this.setState({url: text}) : null} 69 | /> 70 | 76 | 82 | 83 | 84 | ) 85 | } 86 | 87 | } 88 | --------------------------------------------------------------------------------
" + self.content + "