├── .github ├── FUNDING.md └── workflows │ ├── ios.yml │ └── swiftlint.yml ├── .gitignore ├── .swift-format ├── .swiftlint.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Configs └── GlobalConfig.xcconfig ├── LICENSE ├── Mio ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Mio_128.png │ │ ├── Mio_128@2x.png │ │ ├── Mio_16.png │ │ ├── Mio_16@2x.png │ │ ├── Mio_256.png │ │ ├── Mio_256@2x.png │ │ ├── Mio_32.png │ │ ├── Mio_32@2x.png │ │ ├── Mio_512.png │ │ └── Mio_512@2x.png │ ├── Badge │ │ ├── Contents.json │ │ └── Edited.imageset │ │ │ ├── Contents.json │ │ │ └── Edited.pdf │ ├── Color │ │ ├── Contents.json │ │ └── borderedMessageBackground.colorset │ │ │ └── Contents.json │ ├── Contents.json │ └── Icon │ │ ├── AddRoom.imageset │ │ ├── AddRoom.pdf │ │ └── Contents.json │ │ ├── Arrow │ │ ├── Contents.json │ │ └── UpLeft.imageset │ │ │ ├── Contents.json │ │ │ └── UpLeft.pdf │ │ ├── Contents.json │ │ ├── Edit.imageset │ │ ├── Contents.json │ │ └── Edit.pdf │ │ ├── Paperclip.imageset │ │ ├── Contents.json │ │ └── Paperclip.pdf │ │ ├── Paperplane.imageset │ │ ├── Contents.json │ │ └── Paperplane.pdf │ │ ├── Pencil.imageset │ │ ├── Contents.json │ │ └── Pencil.pdf │ │ ├── Smiley.imageset │ │ ├── Contents.json │ │ └── Smiley.pdf │ │ ├── Trash.imageset │ │ ├── Contents.json │ │ └── Trash.pdf │ │ └── User.imageset │ │ ├── Contents.json │ │ └── User.pdf ├── Info.plist ├── InternetAccessPolicy.plist ├── Mio.entitlements └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── Nio.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── Nio.xcscheme │ ├── NioKit-iOS.xcscheme │ └── NioKit-macOS.xcscheme ├── Nio ├── Assets.xcassets │ ├── App Icons │ │ ├── Contents.json │ │ ├── Default.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_60pt@2x.png │ │ │ └── icon_60pt@3x.png │ │ ├── Six Colors Dark.imageset │ │ │ ├── Contents.json │ │ │ ├── Six Colors Dark@2x.png │ │ │ └── Six Colors Dark@3x.png │ │ ├── Six Colors Light.imageset │ │ │ ├── Contents.json │ │ │ ├── Six Colors Light@2x.png │ │ │ └── Six Colors Light@3x.png │ │ └── Sketch.imageset │ │ │ ├── Contents.json │ │ │ ├── Sketch@2x.png │ │ │ └── Sketch@3x.png │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon.png │ │ ├── icon_20pt.png │ │ ├── icon_20pt@2x-1.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_29pt-1.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x-1.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_40pt.png │ │ ├── icon_40pt@2x-1.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ ├── Badge │ │ ├── Contents.json │ │ └── Edited.imageset │ │ │ ├── Contents.json │ │ │ └── Edited.pdf │ ├── Color │ │ ├── Contents.json │ │ └── borderedMessageBackground.colorset │ │ │ └── Contents.json │ ├── Contents.json │ ├── Icon │ │ ├── AddRoom.imageset │ │ │ ├── AddRoom.pdf │ │ │ └── Contents.json │ │ ├── Arrow │ │ │ ├── Contents.json │ │ │ └── UpLeft.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── UpLeft.pdf │ │ ├── Contents.json │ │ ├── Edit.imageset │ │ │ ├── Contents.json │ │ │ └── Edit.pdf │ │ ├── Paperclip.imageset │ │ │ ├── Contents.json │ │ │ └── Paperclip.pdf │ │ ├── Paperplane.imageset │ │ │ ├── Contents.json │ │ │ └── Paperplane.pdf │ │ ├── Pencil.imageset │ │ │ ├── Contents.json │ │ │ └── Pencil.pdf │ │ ├── Smiley.imageset │ │ │ ├── Contents.json │ │ │ └── Smiley.pdf │ │ ├── Trash.imageset │ │ │ ├── Contents.json │ │ │ └── Trash.pdf │ │ └── User.imageset │ │ │ ├── Contents.json │ │ │ └── User.pdf │ └── stub-morpheus.imageset │ │ ├── Contents.json │ │ └── ijUMdVWrjjkJBy4gCmcRdP-1200-80.jpg ├── Authentication │ ├── LoadingView.swift │ └── LoginView.swift ├── Conversations │ ├── ContextMenu │ │ ├── EventContextMenu.swift │ │ ├── EventContextMenuModel.swift │ │ └── ReactionPicker.swift │ ├── Event Views │ │ ├── BadgeView.swift │ │ ├── EventContainerView.swift │ │ ├── GenericEventView.swift │ │ ├── MessageView │ │ │ ├── BorderedMessageView.swift │ │ │ ├── BorderlessMessageView.swift │ │ │ ├── MediaEventView.swift │ │ │ ├── MessageView.swift │ │ │ ├── MessageViewModel.swift │ │ │ └── Reactions │ │ │ │ ├── GroupedReactionsView.swift │ │ │ │ ├── ReactionGroupView.swift │ │ │ │ └── ReactionsListItemView.swift │ │ ├── RedactionEventView.swift │ │ ├── RoomMemberEventView.swift │ │ ├── RoomNameEventView.swift │ │ ├── RoomPowerLevelsEventView.swift │ │ └── RoomTopicEventView.swift │ ├── MessageComposerView.swift │ ├── RecentRoomsView.swift │ ├── RoomListItemView.swift │ ├── RoomView.swift │ └── TypingIndicatorView.swift ├── Extensions │ ├── Color+Named.swift │ ├── Color+allAccent.swift │ ├── ContentSizeCategory.swift │ ├── EnvironmentValues.swift │ ├── NSAttributedString+Extensions.swift │ ├── PreviewProvider+Enumeration.swift │ ├── String+Emoji.swift │ └── String+Extensions.swift ├── Generated │ ├── Assets.swift │ └── Strings.swift ├── Info.plist ├── NewConversation │ └── NewConversationView.swift ├── Nio.entitlements ├── NioApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ └── Alternate Icons │ │ ├── Six Colors Dark │ │ ├── Six Colors Dark@2x.png │ │ └── Six Colors Dark@3x.png │ │ ├── Six Colors Light │ │ ├── Six Colors Light@2x.png │ │ └── Six Colors Light@3x.png │ │ └── Sketch │ │ ├── Sketch@2x.png │ │ └── Sketch@3x.png ├── RootView.swift ├── SceneDelegate.swift ├── Settings │ ├── AppIcon.swift │ └── SettingsView.swift ├── Shapes │ └── IndividuallyRoundedRectangle.swift ├── Shared Views │ ├── AttributedText.swift │ ├── ImagePicker.swift │ ├── MarkdownText.swift │ ├── MessageTextViewWrapper.swift │ ├── MultilineTextField.swift │ ├── ReverseList.swift │ ├── SFSymbol.swift │ └── UITextViewWrapper.swift ├── Supporting Files │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ar.lproj │ │ └── Localizable.strings │ ├── bg.lproj │ │ └── Localizable.strings │ ├── de.lproj │ │ └── Localizable.strings │ ├── en.lproj │ │ └── Localizable.strings │ ├── es.lproj │ │ └── Localizable.strings │ ├── et.lproj │ │ └── Localizable.strings │ ├── eu.lproj │ │ └── Localizable.strings │ ├── fa.lproj │ │ └── Localizable.strings │ ├── fi.lproj │ │ └── Localizable.strings │ ├── fr.lproj │ │ └── Localizable.strings │ ├── fy.lproj │ │ └── Localizable.strings │ ├── hr.lproj │ │ └── Localizable.strings │ ├── hu.lproj │ │ └── Localizable.strings │ ├── id.lproj │ │ └── Localizable.strings │ ├── it.lproj │ │ └── Localizable.strings │ ├── ja.lproj │ │ └── Localizable.strings │ ├── ko.lproj │ │ └── Localizable.strings │ ├── lo.lproj │ │ └── Localizable.strings │ ├── lt.lproj │ │ └── Localizable.strings │ ├── nb.lproj │ │ └── Localizable.strings │ ├── nl.lproj │ │ └── Localizable.strings │ ├── nn.lproj │ │ └── Localizable.strings │ ├── pl.lproj │ │ └── Localizable.strings │ ├── pt-BR.lproj │ │ └── Localizable.strings │ ├── pt.lproj │ │ └── Localizable.strings │ ├── ru.lproj │ │ └── Localizable.strings │ ├── si.lproj │ │ └── Localizable.strings │ ├── sk.lproj │ │ └── Localizable.strings │ ├── sq.lproj │ │ └── Localizable.strings │ ├── sv.lproj │ │ └── Localizable.strings │ ├── ta.lproj │ │ └── Localizable.strings │ ├── tr.lproj │ │ └── Localizable.strings │ ├── uk.lproj │ │ └── Localizable.strings │ ├── vi.lproj │ │ └── Localizable.strings │ ├── zh-Hans.lproj │ │ └── Localizable.strings │ └── zh-Hant.lproj │ │ └── Localizable.strings └── Utility │ ├── Formatter.swift │ ├── GroupedEvents.swift │ ├── GroupingIterator.swift │ ├── MXURL.swift │ ├── PeekableIterator.swift │ └── TypedEvents.swift ├── NioKit ├── Configuration │ └── Configuration.swift ├── Extensions │ ├── MX+Identifiable.swift │ ├── MXClient+Publisher.swift │ ├── MXCredentials+Keychain.swift │ ├── MXEvent+Extensions.swift │ ├── UXKit.swift │ └── UserDefaults.swift ├── Models │ ├── Custom Events │ │ ├── CustomEvent.swift │ │ ├── EditEvent.swift │ │ └── ReactionEvent.swift │ ├── EventCollection.swift │ ├── NIORoom.swift │ ├── NIORoomSummary.swift │ └── Reaction.swift └── Session │ └── AccountStore.swift ├── NioKitTests ├── Info.plist └── NioKitTests.swift ├── NioShareExtension ├── GetURL.js ├── Info.plist ├── NioShareExtension.entitlements ├── ShareContentView.swift └── ShareViewController.swift ├── NioTests ├── EventCollectionTests.swift ├── GroupingIteratorTests.swift ├── Info.plist ├── Mocks │ └── MockEvent.swift ├── PeekableIteratorTests.swift ├── RoomMemberEventViewTests.swift └── TypedEventsTests.swift ├── README.md ├── Resources ├── AppIcon │ ├── Default │ │ ├── Default.sketch │ │ ├── Icon.png │ │ ├── Rounded_1024.png │ │ ├── Rounded_500.png │ │ ├── icon_20pt.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_40pt.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ ├── Six Colors Dark │ │ ├── Six Colors Dark.png │ │ ├── Six Colors Dark@2x.png │ │ ├── Six Colors Dark@3x.png │ │ └── Six Colors.sketch │ ├── Six Colors Light │ │ ├── Six Colors Light.png │ │ ├── Six Colors Light@2x.png │ │ └── Six Colors Light@3x.png │ ├── Sketch │ │ ├── Sketch.png │ │ ├── Sketch@2x.png │ │ └── Sketch@3x.png │ └── attribution.txt ├── Application.sketch └── Base │ ├── Chat.pdf │ ├── Chat.png │ ├── Chat.sketch │ ├── Chat.svg │ └── attribution.txt ├── fastlane ├── Fastfile └── README.md └── swiftgen.yml /.github/FUNDING.md: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | liberapay: nio 15 | -------------------------------------------------------------------------------- /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: stable 7 | 8 | jobs: 9 | build_and_test: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Version 14 | run: | 15 | sudo xcode-select --switch /Applications/Xcode_13.1.app 16 | xcode-select --print-path 17 | xcodebuild -version 18 | - name: Prerequisites 19 | run: | 20 | gem install xcpretty 21 | - name: Build 22 | run: | 23 | set -o pipefail 24 | xcodebuild -scheme Nio -destination 'platform=iOS Simulator,name=iPhone 12' build | xcpretty --color 25 | - name: Test 26 | run: | 27 | set -o pipefail 28 | xcodebuild -scheme Nio -destination 'platform=iOS Simulator,name=iPhone 12' test | xcpretty --color --test 29 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | swiftlint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: lint changed files 11 | uses: norio-nomura/action-swiftlint@3.1.0 12 | env: 13 | DIFF_BASE: ${{ github.base_ref }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*/xcuserdata 2 | Pods/CocoaPodsKeys 3 | fastlane/Appfile 4 | Configs/LocalConfig.xcconfig 5 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 4 7 | }, 8 | "indentConditionalCompilationBlocks" : true, 9 | "lineBreakAroundMultilineExpressionChainComponents" : false, 10 | "lineBreakBeforeControlFlowKeywords" : false, 11 | "lineBreakBeforeEachArgument" : false, 12 | "lineBreakBeforeEachGenericRequirement" : false, 13 | "lineLength" : 120, 14 | "maximumBlankLines" : 1, 15 | "prioritizeKeepingFunctionOutputTogether" : false, 16 | "respectsExistingLineBreaks" : true, 17 | "rules" : { 18 | "AllPublicDeclarationsHaveDocumentation" : false, 19 | "AlwaysUseLowerCamelCase" : false, 20 | "AmbiguousTrailingClosureOverload" : true, 21 | "BeginDocumentationCommentWithOneLineSummary" : true, 22 | "DoNotUseSemicolons" : true, 23 | "DontRepeatTypeInStaticProperties" : true, 24 | "FileprivateAtFileScope" : true, 25 | "FullyIndirectEnum" : true, 26 | "GroupNumericLiterals" : true, 27 | "IdentifiersMustBeASCII" : true, 28 | "NeverForceUnwrap" : false, 29 | "NeverUseForceTry" : true, 30 | "NeverUseImplicitlyUnwrappedOptionals" : true, 31 | "NoAccessLevelOnExtensionDeclaration" : true, 32 | "NoBlockComments" : true, 33 | "NoCasesWithOnlyFallthrough" : true, 34 | "NoEmptyTrailingClosureParentheses" : true, 35 | "NoLabelsInCasePatterns" : true, 36 | "NoLeadingUnderscores" : true, 37 | "NoParensAroundConditions" : true, 38 | "NoVoidReturnOnFunctionSignature" : true, 39 | "OneCasePerLine" : true, 40 | "OneVariableDeclarationPerLine" : true, 41 | "OnlyOneTrailingClosureArgument" : true, 42 | "OrderedImports" : true, 43 | "ReturnVoidInsteadOfEmptyTuple" : true, 44 | "UseLetInEveryBoundCaseVariable" : true, 45 | "UseShorthandTypeNames" : true, 46 | "UseSingleLinePropertyGetter" : true, 47 | "UseSynthesizedInitializer" : true, 48 | "UseTripleSlashForDocumentationComments" : true, 49 | "ValidateDocumentationComments" : true 50 | }, 51 | "tabWidth" : 8, 52 | "version" : 1 53 | } 54 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_comma 3 | - empty_enum_arguments 4 | - void_return 5 | - redundant_optional_initialization 6 | - nesting 7 | - redundant_discardable_let 8 | - opening_brace 9 | - multiple_closures_with_trailing_closure 10 | excluded: 11 | - Pods 12 | line_length: 13 | warning: 140 14 | error: 140 15 | identifier_name: 16 | excluded: 17 | - 'id' 18 | - 'i' 19 | - 'x' 20 | - 'y' 21 | - 'z' 22 | type_name: 23 | max_length: 60 24 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at coc@nio.chat. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nio 2 | 3 | First off, thanks for taking the time to contribute! 😍🥳 4 | There are several ways you can leave your mark, we're extremely glad to have you! 5 | 6 | ## Feedback 7 | 8 | At the current time, Nio is not yet ready for an actual release on the App Store. Many essential features are [still 9 | missing](https://github.com/niochat/nio/issues?q=is%3Aopen+is%3Aissue+label%3Aessential). Current builds are distributed via 10 | [TestFlight](https://testflight.apple.com/join/KlXr3kKz). We heavily rely on people installing these builds and telling us 11 | if other things are not yet working as expected or if there are bugs in the existing features. 12 | 13 | Should you come across anything in that regard, be it as simple as a typo in the app's description, behavior that doesn't feel 14 | correct, a crash of the application or *anything* else, please reach out. Either by [opening an 15 | issue](https://github.com/niochat/nio/issues/new) directly on GitHub, sending a message in our Matrix room 16 | [#niochat:matrix.org](https://matrix.to/#/#niochat:matrix.org) or via mail to 17 | [team@nio.chat](mailto:team@nio.chat). 18 | 19 | Please also feel free to comment on existing issues and pull requests. A new opinion, viewpoint or even just an upvote is 20 | always good to have. 21 | 22 | We know we're still far away from the goal, but Nio is aiming to be as inclusive as possible and while trying to do our best, 23 | we also rely on people like you telling us if things are anywhere between unclear and confusing or just downright wrong. 24 | 25 | Thank you! ❤️ 26 | 27 | ## Localization 28 | 29 | As Nio aims to be accessible by anyone from around the globe, it's important to have Nio speak as many languages as possible. 30 | Nio uses a tool called Weblate running on [translate.riot.im](https://translate.riot.im/engage/nio) to manage 31 | translation files. Unfortunately you do need an account there, but it's of course free and easy to register for one. After 32 | doing that you can fix and modify existing translations or add new languages altogether, both of which are *highly* 33 | appreciated! 34 | 35 | If Weblate isn't to your liking and you prefer a text editor and git, feel free to edit the translations directly in the 36 | project's repository. They're synchronized both ways. 37 | 38 | This is the current status of the translations: 39 | 40 | [![Translation status](https://translate.riot.im/widgets/nio/-/nio/multi-auto.svg)](https://translate.riot.im/engage/nio/?utm_source=widget) 41 | 42 | ## Custom App Icons 43 | 44 | Nio includes a few basic app icons, but a bigger selection of community icons would be fantastic. All that's necessary to include a new icon is a 1024x1024px PNG file. Please don't include any rounded edges, iOS does that automatically. Check the [Resources](https://github.com/niochat/nio/tree/stable/Resources) directory for the existing icons and the base chat bubbles in different formats if you want to build on that. You can send the finished file to our Matrix room or mail address (all listed on [nio.chat](https://nio.chat)) or just [open a new issue](https://github.com/niochat/nio/issues/new) on GitHub 😊 45 | 46 | ## Implementation 47 | 48 | Nio has (and probably always will have 😅) a [long list of open issues](https://github.com/niochat/nio/issues). You are more 49 | than *extremely* welcome to tackle any of these and send pull requests. 50 | 51 | ### Dependencies 52 | Nio manages its dependencies via Swift package manager, which Xcode handles automatically. The `matrix-ios-sdk` 53 | however doesn't directly support SwiftPM, so we build our own compatible framework and pull it from [niochat/MatrixSDK](https://github.com/niochat/MatrixSDK). 54 | 55 | ### Automatic Signing 56 | In order to use automatic signing in Xcode, it is possible to create 57 | `Configs/LocalConfig.xcconfig` and override the default values. For more info 58 | have a look [here](https://www.matrixprojects.net/p/xcconfig-for-shared-projects/). 59 | 60 | If you are unsure where to start or have other questions, please reach out, preferably via Matrix in 61 | [#niochat:matrix.org](https://matrix.to/#/#niochat:matrix.org) or whatever works for you 😊 62 | -------------------------------------------------------------------------------- /Configs/GlobalConfig.xcconfig: -------------------------------------------------------------------------------- 1 | // Configuration settings file format documentation can be found at: 2 | // https://help.apple.com/xcode/#/dev745c5c974 3 | 4 | // Nio: When working on your fork of Nio, create the `LocalConfig.xcconfig` 5 | // file, and set the `NIO_NAMESPACE` to some reverse DNS you own. 6 | // The same goes for the `DEVELOPMENT_TEAM`. 7 | // Example `LocalConfig.xcconfig`: 8 | // NIO_NAMESPACE = com.example.nio 9 | // DEVELOPMENT_TEAM = Z123456789 10 | 11 | NIO_NAMESPACE = com.example.nio 12 | DEVELOPMENT_TEAM = Z123456789 13 | 14 | APPGROUP = $(NIO_NAMESPACE) 15 | PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS 16 | MAC_PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).mio 17 | SHARE_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioShareExtension 18 | CODE_SIGN_STYLE = Manual 19 | 20 | #include? "LocalConfig.xcconfig" 21 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Mio_16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Mio_16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Mio_32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Mio_32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Mio_128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Mio_128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Mio_256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Mio_256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Mio_512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Mio_512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_128.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_128@2x.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_16.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_16@2x.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_256.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_256@2x.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_32.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_32@2x.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_512.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/AppIcon.appiconset/Mio_512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/AppIcon.appiconset/Mio_512@2x.png -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Badge/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Badge/Edited.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Edited.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Badge/Edited.imageset/Edited.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Badge/Edited.imageset/Edited.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Color/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Color/borderedMessageBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.900", 9 | "green" : "0.864", 10 | "red" : "0.855" 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.250", 27 | "green" : "0.226", 28 | "red" : "0.220" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/AddRoom.imageset/AddRoom.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/AddRoom.imageset/AddRoom.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/AddRoom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AddRoom.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Arrow/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Arrow/UpLeft.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "UpLeft.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Arrow/UpLeft.imageset/UpLeft.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Arrow/UpLeft.imageset/UpLeft.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Edit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Edit.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Edit.imageset/Edit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Edit.imageset/Edit.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Paperclip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Paperclip.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Paperclip.imageset/Paperclip.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Paperclip.imageset/Paperclip.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Paperplane.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Paperplane.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Paperplane.imageset/Paperplane.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Paperplane.imageset/Paperplane.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Pencil.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Pencil.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Pencil.imageset/Pencil.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Pencil.imageset/Pencil.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Smiley.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Smiley.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Smiley.imageset/Smiley.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Smiley.imageset/Smiley.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Trash.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/Trash.imageset/Trash.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/Trash.imageset/Trash.pdf -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/User.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "User.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mio/Assets.xcassets/Icon/User.imageset/User.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Mio/Assets.xcassets/Icon/User.imageset/User.pdf -------------------------------------------------------------------------------- /Mio/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSMinimumSystemVersion 22 | $(MACOSX_DEPLOYMENT_TARGET) 23 | NSHumanReadableCopyright 24 | Copyright © 2021 Kilian Koeltzsch. All rights reserved. 25 | 26 | AppIdentifierPrefix 27 | $(AppIdentifierPrefix) 28 | TeamIdentifierPrefix 29 | $(TeamIdentifierPrefix) 30 | BugReporterURL 31 | https://github.com/niochat/nio/issues 32 | 33 | NSAppTransportSecurity 34 | 35 | NSAllowsArbitraryLoads 36 | 37 | 38 | 39 | AppGroup 40 | $(APPGROUP) 41 | DevelopmentTeam 42 | $(DEVELOPMENT_TEAM) 43 | 44 | 45 | -------------------------------------------------------------------------------- /Mio/InternetAccessPolicy.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ApplicationDescription 6 | Mio is a client for the Matrix messaging service. 7 | Connections 8 | 9 | 10 | IsIncoming 11 | 12 | Host 13 | * 14 | Port 15 | 443 16 | NetworkProtocol 17 | TCP 18 | Relevance 19 | Essential 20 | Purpose 21 | Mio needs the connection to contact Matrix services. 22 | DenyConsequences 23 | If you deny this connection, Mio won't be able to send or receive messages. 24 | 25 | 26 | IsIncoming 27 | 28 | Host 29 | * 30 | Port 31 | 80 32 | NetworkProtocol 33 | TCP 34 | Relevance 35 | default 36 | Purpose 37 | Mio needs the connections to retrieve arbitrary web resources referred to by users or bots in Matrix messages. 38 | DenyConsequences 39 | If you deny this connection, Mio won't be able to receive assets from the web for inline display. 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Mio/Mio.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.personal-information.addressbook 12 | 13 | com.apple.security.personal-information.photos-library 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Mio/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Nio.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Nio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Nio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "BlurHash", 6 | "repositoryURL": "https://github.com/niochat/BlurHash", 7 | "state": { 8 | "branch": null, 9 | "revision": "1c54f004fa1314ebcf678fad33381afb449a79b9", 10 | "version": "0.1.0" 11 | } 12 | }, 13 | { 14 | "package": "CommonMark", 15 | "repositoryURL": "https://github.com/SwiftDocOrg/CommonMark.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "35718fc895ff25908628a47b44ae74aba75b6770", 19 | "version": "0.3.2" 20 | } 21 | }, 22 | { 23 | "package": "CommonMarkAttributedString", 24 | "repositoryURL": "https://github.com/mattt/CommonMarkAttributedString", 25 | "state": { 26 | "branch": null, 27 | "revision": "578419f521d16fd5fb6e757e7f5a6a4748fc0c38", 28 | "version": "0.2.0" 29 | } 30 | }, 31 | { 32 | "package": "KeychainAccess", 33 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", 34 | "state": { 35 | "branch": null, 36 | "revision": "654d52d30f3dd4592e944c3e0bccb53178c992f6", 37 | "version": "4.2.1" 38 | } 39 | }, 40 | { 41 | "package": "MatrixSDK", 42 | "repositoryURL": "https://github.com/niochat/MatrixSDK", 43 | "state": { 44 | "branch": null, 45 | "revision": "faab7d8d55910f40589111caf9392d67c24889a9", 46 | "version": "0.20.14" 47 | } 48 | }, 49 | { 50 | "package": "SDWebImage", 51 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "e857b78ca4c8afe616b794aeb090d40f4e348072", 55 | "version": "5.10.0" 56 | } 57 | }, 58 | { 59 | "package": "SDWebImageSwiftUI", 60 | "repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "4c7f169e39bc35d6b80d42b8eb8301bee9cd0907", 64 | "version": "1.5.0" 65 | } 66 | }, 67 | { 68 | "package": "cmark", 69 | "repositoryURL": "https://github.com/SwiftDocOrg/swift-cmark.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "1168665f6b36be747ffe6b7b90bc54cfc17f42b7", 73 | "version": "0.28.3+20200207.1168665" 74 | } 75 | }, 76 | { 77 | "package": "Introspect", 78 | "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", 79 | "state": { 80 | "branch": null, 81 | "revision": "36ecf80429d00a4cd1e81fbfe4655b1d99ebd651", 82 | "version": "0.1.2" 83 | } 84 | }, 85 | { 86 | "package": "SwiftyBeaver", 87 | "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver", 88 | "state": { 89 | "branch": null, 90 | "revision": "2c039501d6eeb4d4cd4aec4a8d884ad28862e044", 91 | "version": "1.9.5" 92 | } 93 | } 94 | ] 95 | }, 96 | "version": 1 97 | } 98 | -------------------------------------------------------------------------------- /Nio.xcodeproj/xcshareddata/xcschemes/NioKit-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Nio.xcodeproj/xcshareddata/xcschemes/NioKit-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Default.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "icon_60pt@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "icon_60pt@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Default.imageset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Default.imageset/icon_60pt@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Default.imageset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Default.imageset/icon_60pt@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Six Colors Dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Six Colors Dark@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "Six Colors Dark@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Six Colors Dark.imageset/Six Colors Dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Six Colors Dark.imageset/Six Colors Dark@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Six Colors Dark.imageset/Six Colors Dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Six Colors Dark.imageset/Six Colors Dark@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Six Colors Light.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Six Colors Light@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "Six Colors Light@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Six Colors Light.imageset/Six Colors Light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Six Colors Light.imageset/Six Colors Light@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Six Colors Light.imageset/Six Colors Light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Six Colors Light.imageset/Six Colors Light@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Sketch.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Sketch@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "Sketch@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "original" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Sketch.imageset/Sketch@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Sketch.imageset/Sketch@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/App Icons/Sketch.imageset/Sketch@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/App Icons/Sketch.imageset/Sketch@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_20pt@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_20pt@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_29pt.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_29pt@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "icon_29pt@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_40pt@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "icon_40pt@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_60pt@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "icon_60pt@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon_20pt.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "icon_20pt@2x-1.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon_29pt-1.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "icon_29pt@2x-1.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon_40pt.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "icon_40pt@2x-1.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon_76pt.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "icon_76pt@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "icon_83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt-1.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Badge/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Badge/Edited.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Edited.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Badge/Edited.imageset/Edited.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Badge/Edited.imageset/Edited.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Color/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Color/borderedMessageBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "0.855", 13 | "alpha" : "1.000", 14 | "blue" : "0.900", 15 | "green" : "0.864" 16 | } 17 | } 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "dark" 25 | } 26 | ], 27 | "color" : { 28 | "color-space" : "srgb", 29 | "components" : { 30 | "red" : "0.220", 31 | "alpha" : "1.000", 32 | "blue" : "0.250", 33 | "green" : "0.226" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/AddRoom.imageset/AddRoom.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/AddRoom.imageset/AddRoom.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/AddRoom.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AddRoom.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Arrow/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Arrow/UpLeft.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "UpLeft.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Arrow/UpLeft.imageset/UpLeft.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Arrow/UpLeft.imageset/UpLeft.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Edit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Edit.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Edit.imageset/Edit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Edit.imageset/Edit.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Paperclip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Paperclip.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Paperclip.imageset/Paperclip.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Paperclip.imageset/Paperclip.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Paperplane.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Paperplane.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Paperplane.imageset/Paperplane.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Paperplane.imageset/Paperplane.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Pencil.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Pencil.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Pencil.imageset/Pencil.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Pencil.imageset/Pencil.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Smiley.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Smiley.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Smiley.imageset/Smiley.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Smiley.imageset/Smiley.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Trash.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/Trash.imageset/Trash.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/Trash.imageset/Trash.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/User.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "User.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Nio/Assets.xcassets/Icon/User.imageset/User.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/Icon/User.imageset/User.pdf -------------------------------------------------------------------------------- /Nio/Assets.xcassets/stub-morpheus.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ijUMdVWrjjkJBy4gCmcRdP-1200-80.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Nio/Assets.xcassets/stub-morpheus.imageset/ijUMdVWrjjkJBy4gCmcRdP-1200-80.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Assets.xcassets/stub-morpheus.imageset/ijUMdVWrjjkJBy4gCmcRdP-1200-80.jpg -------------------------------------------------------------------------------- /Nio/Authentication/LoadingView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | import NioKit 4 | 5 | struct LoadingView: View { 6 | @EnvironmentObject private var store: AccountStore 7 | 8 | private let loadingEmoji = [ 9 | "🧑‍🎤", 10 | "🧑‍🏭", 11 | "🧑‍🔧", 12 | "🧑‍💻", 13 | ] 14 | 15 | private let loadingMessages = [ 16 | L10n.Loading._1, 17 | L10n.Loading._2, 18 | L10n.Loading._3, 19 | L10n.Loading._4, 20 | ] 21 | 22 | private var randomLoadingMessage: String { 23 | "\(loadingEmoji.randomElement()!) \(loadingMessages.randomElement()!)" 24 | } 25 | 26 | var body: some View { 27 | VStack { 28 | Spacer() 29 | 30 | ProgressView().padding(1) 31 | 32 | Text(self.randomLoadingMessage) 33 | .bold() 34 | .padding(.horizontal) 35 | 36 | Spacer() 37 | 38 | Button(action: self.store.logout) { 39 | Text(verbatim: L10n.Loading.cancel).font(.callout) 40 | } 41 | .padding() 42 | } 43 | } 44 | } 45 | 46 | struct LoadingView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | LoadingView() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Nio/Conversations/ContextMenu/EventContextMenu.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MatrixSDK 3 | 4 | private struct EventContextMenuViewModel { 5 | fileprivate let canReact: Bool 6 | fileprivate let canReply: Bool 7 | fileprivate let canEdit: Bool 8 | fileprivate let canRedact: Bool 9 | 10 | init(event: MXEvent, userId: String) { 11 | var canReact = false 12 | let canReply = false 13 | var canEdit = false 14 | var canRedact = false 15 | 16 | // The correct way to check if replying is possible is `-[MXRoom canReplyToEvent:]`. 17 | 18 | let reactableEvents = [ 19 | kMXEventTypeStringRoomMessage 20 | ] 21 | 22 | if reactableEvents.contains(event.type ?? "") { 23 | canReact = true 24 | // canReply = true 25 | } 26 | 27 | // TODO: Redacting messages is a powerlevel thing, you can't only redact your own. 28 | if event.sender == userId 29 | && reactableEvents.contains(event.type ?? "") 30 | && !event.isRedactedEvent() { 31 | canEdit = true 32 | canRedact = true 33 | } 34 | 35 | if event.isMediaAttachment() { 36 | canEdit = false 37 | } 38 | 39 | self.canReact = canReact 40 | self.canReply = canReply 41 | self.canEdit = canEdit 42 | self.canRedact = canRedact 43 | } 44 | } 45 | 46 | struct EventContextMenu: View { 47 | private var model: EventContextMenuViewModel 48 | 49 | typealias Action = () -> Void 50 | 51 | private let onReact: Action 52 | private let onReply: Action 53 | private let onEdit: Action 54 | private let onRedact: Action 55 | 56 | init(model: EventContextMenuModel) { 57 | self.init(event: model.event, 58 | userId: model.userId, 59 | onReact: model.onReact, 60 | onReply: model.onReply, 61 | onEdit: model.onEdit, 62 | onRedact: model.onRedact) 63 | } 64 | 65 | init(event: MXEvent, 66 | userId: String, 67 | onReact: @escaping Action, 68 | onReply: @escaping Action, 69 | onEdit: @escaping Action, 70 | onRedact: @escaping Action 71 | ) { 72 | self.model = EventContextMenuViewModel(event: event, userId: userId) 73 | self.onReact = onReact 74 | self.onReply = onReply 75 | self.onEdit = onEdit 76 | self.onRedact = onRedact 77 | } 78 | 79 | var body: some View { 80 | Group { 81 | if model.canReact { 82 | Button(action: onReact, label: { 83 | Text(verbatim: L10n.Event.ContextMenu.addReaction) 84 | Image(Asset.Icon.smiley.name) 85 | .resizable() 86 | .frame(width: 30.0, height: 30.0) 87 | }) 88 | } 89 | if model.canReply { 90 | Button(action: onReply, label: { 91 | Text(verbatim: L10n.Event.ContextMenu.reply) 92 | Image(Asset.Icon.Arrow.upLeft.name) 93 | .resizable() 94 | .frame(width: 30.0, height: 30.0) 95 | }) 96 | } 97 | if model.canEdit { 98 | Button(action: onEdit, label: { 99 | Text(verbatim: L10n.Event.ContextMenu.edit) 100 | Image(Asset.Icon.pencil.name) 101 | .resizable() 102 | .frame(width: 30.0, height: 30.0) 103 | }) 104 | } 105 | if model.canRedact { 106 | Button(action: onRedact, label: { 107 | Text(verbatim: L10n.Event.ContextMenu.remove) 108 | Image(Asset.Icon.trash.name) 109 | .resizable() 110 | .frame(width: 30.0, height: 30.0) 111 | }) 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Nio/Conversations/ContextMenu/EventContextMenuModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MatrixSDK 3 | 4 | struct EventContextMenuModel { 5 | typealias Action = () -> Void 6 | 7 | var event: MXEvent 8 | var userId: String 9 | 10 | var onReact: Action 11 | var onReply: Action 12 | var onEdit: Action 13 | var onRedact: Action 14 | } 15 | 16 | extension EventContextMenuModel { 17 | static var previewModel: EventContextMenuModel { 18 | EventContextMenuModel(event: MXEvent(), userId: "Jane", onReact: {}, onReply: {}, onEdit: {}, onRedact: {}) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Nio/Conversations/ContextMenu/ReactionPicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ReactionPicker: View { 4 | let emoji = ["👍", "👎", "😄", "🎉", "❤️", "🚀", "👀"] 5 | 6 | let picked: (String) -> Void 7 | 8 | var body: some View { 9 | VStack { 10 | Text(verbatim: L10n.ReactionPicker.title) 11 | .foregroundColor(.gray) 12 | .font(.headline) 13 | .padding(.bottom, 30) 14 | HStack(spacing: 10) { 15 | ForEach(emoji, id: \.self) { emoji in 16 | Button(action: { self.picked(emoji) }, 17 | label: { 18 | Text(emoji) 19 | .font(.largeTitle) 20 | }) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | struct ReactionPicker_Previews: PreviewProvider { 28 | static var previews: some View { 29 | ReactionPicker(picked: { _ in }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/BadgeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | 4 | struct BadgeView: View { 5 | let image: Image 6 | let foregroundColor: Color 7 | let backgroundColor: Color 8 | 9 | var body: some View { 10 | let lineWidth: CGFloat = 3.0 11 | 12 | let circle = Circle() 13 | .stroke(foregroundColor, lineWidth: lineWidth) 14 | .overlay( 15 | Circle() 16 | .fill(backgroundColor) 17 | ) 18 | .padding(lineWidth) 19 | 20 | return image 21 | .resizable() 22 | .aspectRatio(contentMode: .fit) 23 | .foregroundColor(foregroundColor) 24 | .padding(4.0) 25 | .background(circle) 26 | } 27 | } 28 | 29 | struct BadgeView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | let image = Image(Asset.Badge.edited.name) 32 | let foregroundColor = Color(UXColor.lightGray) 33 | let backgroundColor = Color(UXColor.darkGray) 34 | 35 | return BadgeView( 36 | image: image, 37 | foregroundColor: foregroundColor, 38 | backgroundColor: backgroundColor 39 | ) 40 | .previewLayout(.sizeThatFits) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/GenericEventView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SDWebImageSwiftUI 3 | 4 | import NioKit 5 | 6 | struct GenericEventView: View { 7 | @EnvironmentObject private var store: AccountStore 8 | 9 | let text: String 10 | var image: MXURL? 11 | private var imageURL: URL? { 12 | return store.client?.homeserver 13 | .flatMap(URL.init(string:)) 14 | .flatMap { image?.contentURL(on: $0) } 15 | } 16 | 17 | var body: some View { 18 | HStack(spacing: 4) { 19 | Spacer() 20 | if imageURL != nil { 21 | WebImage(url: imageURL!) 22 | .resizable() 23 | .aspectRatio(contentMode: .fill) 24 | .frame(width: 15, height: 15) 25 | .mask(Circle()) 26 | } 27 | Text(text) 28 | .font(.caption) 29 | .foregroundColor(.gray) 30 | Spacer() 31 | } 32 | .padding(.vertical, 3) 33 | } 34 | } 35 | 36 | struct GenericEventView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | Group { 39 | GenericEventView(text: "Ping joined", image: .nioIcon) 40 | GenericEventView(text: "Ping changed the topic to '🐧'") 41 | 42 | VStack(spacing: 0) { 43 | GenericEventView(text: "Ping joined") 44 | GenericEventView(text: "Ping joined") 45 | GenericEventView(text: "Ping joined") 46 | } 47 | } 48 | .accentColor(.purple) 49 | // .environment(\.colorScheme, .dark) 50 | .padding() 51 | .previewLayout(.sizeThatFits) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/MessageView/MessageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MatrixSDK 3 | 4 | import NioKit 5 | 6 | struct MessageView: View where Model: MessageViewModelProtocol { 7 | @Environment(\.colorScheme) private var colorScheme 8 | @Environment(\.sizeCategory) private var sizeCategory: ContentSizeCategory 9 | @Environment(\.userId) private var userId 10 | 11 | @Binding var model: Model 12 | let contextMenuModel: EventContextMenuModel 13 | let connectedEdges: ConnectedEdges 14 | var isEdited = false 15 | 16 | private var isMe: Bool { 17 | model.sender == userId 18 | } 19 | 20 | var body: some View { 21 | if model.isEmoji { 22 | let messageView = BorderlessMessageView( 23 | model: model, 24 | contextMenuModel: contextMenuModel, 25 | connectedEdges: connectedEdges, 26 | isEdited: self.isEdited 27 | ) 28 | if isMe { 29 | HStack { 30 | Spacer() 31 | messageView 32 | } 33 | } else { 34 | HStack { 35 | messageView 36 | Spacer() 37 | } 38 | } 39 | } else { 40 | let messageView = BorderedMessageView( 41 | model: model, 42 | contextMenuModel: contextMenuModel, 43 | connectedEdges: connectedEdges, 44 | isEdited: self.isEdited 45 | ) 46 | if isMe { 47 | HStack { 48 | Spacer() 49 | messageView 50 | } 51 | } else { 52 | HStack { 53 | messageView 54 | Spacer() 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | struct MessageView_Previews: PreviewProvider { 62 | private struct MessageViewModel: MessageViewModelProtocol { 63 | var id: String 64 | var text: String 65 | var sender: String 66 | var sentState: MXEventSentState 67 | var showSender: Bool 68 | var timestamp: String 69 | var reactions: [Reaction] 70 | } 71 | 72 | static func message(text: String, 73 | sender: String, 74 | userId: String, 75 | showSender: Bool, 76 | reactions: [Reaction] 77 | ) -> some View { 78 | MessageView( 79 | model: .constant(MessageViewModel( 80 | id: "0", 81 | text: text, 82 | sender: sender, 83 | sentState: MXEventSentStateSent, 84 | showSender: showSender, 85 | timestamp: "12:29", 86 | reactions: reactions 87 | )), 88 | contextMenuModel: .previewModel, 89 | connectedEdges: [] 90 | ) 91 | .padding() 92 | .environment(\.userId, userId) 93 | } 94 | 95 | static var previews: some View { 96 | Group { 97 | message( 98 | text: "Hi there!", 99 | sender: "John Doe", 100 | userId: "Jane Doe", 101 | showSender: true, 102 | reactions: [] 103 | ) 104 | 105 | message( 106 | text: "👋", 107 | sender: "John Doe", 108 | userId: "Jane Doe", 109 | showSender: false, 110 | reactions: [] 111 | ) 112 | } 113 | .accentColor(.purple) 114 | .previewLayout(.sizeThatFits) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/MessageView/MessageViewModel.swift: -------------------------------------------------------------------------------- 1 | import MatrixSDK 2 | import NioKit 3 | 4 | protocol MessageViewModelProtocol { 5 | var id: String { get } 6 | var text: String { get } 7 | var sender: String { get } 8 | var sentState: MXEventSentState { get } 9 | var showSender: Bool { get } 10 | var timestamp: String { get } 11 | 12 | var reactions: [Reaction] { get } 13 | } 14 | 15 | extension MessageViewModelProtocol { 16 | var isEmoji: Bool { 17 | (text.count <= 3) && text.containsOnlyEmoji 18 | } 19 | } 20 | 21 | struct MessageViewModel: MessageViewModelProtocol { 22 | enum Error: Swift.Error { 23 | case invalidEventType(MXEventType) 24 | 25 | var localizedDescription: String { 26 | switch self { 27 | case .invalidEventType(let type): 28 | return "Expected message event, found \(type)" 29 | } 30 | } 31 | } 32 | 33 | var id: String { 34 | event.eventId 35 | } 36 | 37 | var text: String { 38 | if !event.isEdit() { 39 | return (event.content["body"] as? String).map { 40 | $0.trimmingCharacters(in: .whitespacesAndNewlines) 41 | } ?? "Error: expected string body" 42 | } else { 43 | let newContent = event.content["m.new_content"]! as? NSDictionary 44 | return (newContent?["body"] as? String).map { 45 | $0.trimmingCharacters(in: .whitespacesAndNewlines) 46 | } ?? "Error: expected string body" 47 | } 48 | } 49 | 50 | var sender: String { 51 | event.sender 52 | } 53 | 54 | var sentState: MXEventSentState { 55 | event.sentState 56 | } 57 | 58 | var showSender: Bool 59 | 60 | var timestamp: String { 61 | Formatter.string(for: event.timestamp, timeStyle: .short) 62 | } 63 | 64 | var reactions: [Reaction] 65 | 66 | private let event: MXEvent 67 | 68 | public init(event: MXEvent, reactions: [Reaction], showSender: Bool) throws { 69 | try Self.validate(event: event) 70 | 71 | self.event = event 72 | self.reactions = reactions 73 | self.showSender = showSender 74 | } 75 | 76 | private static func validate(event: MXEvent) throws { 77 | // NOTE: For as long as https://github.com/matrix-org/matrix-ios-sdk/pull/843 78 | // remains unresolved keep in mind that 79 | // `.keyVerificationStart`, `.keyVerificationAccept`, `.keyVerificationKey`, 80 | // `.keyVerificationMac`, `.keyVerificationCancel` & `.reaction` 81 | // may get wrongly recognized as `.custom(…)`, instead. 82 | // FIXME: Remove comment when linked bug fix has been merged. 83 | let eventType = MXEventType(identifier: event.type) 84 | guard eventType == .roomMessage else { 85 | throw Error.invalidEventType(eventType) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/MessageView/Reactions/ReactionGroupView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ReactionGroupView: View { 4 | private let text: String 5 | private let count: Int 6 | 7 | private let backgroundColor: Color 8 | 9 | init(text: String, count: Int, backgroundColor: Color) { 10 | assert(count > 0, "Expected non-zero positive integer") 11 | 12 | self.text = text 13 | self.count = count 14 | self.backgroundColor = backgroundColor 15 | } 16 | 17 | var body: some View { 18 | return HStack(spacing: 1) { 19 | Text(self.text) 20 | Text(String(self.count)) 21 | } 22 | .font(.footnote) 23 | .padding(EdgeInsets(top: 4.0, leading: 8.0, bottom: 4.0, trailing: 8.0)) 24 | .background( 25 | RoundedRectangle(cornerRadius: 30) 26 | .fill(self.backgroundColor) 27 | ) 28 | } 29 | } 30 | 31 | struct ReactionGroupView_Previews: PreviewProvider { 32 | static var reactionView: some View { 33 | ReactionGroupView( 34 | text: "💩", 35 | count: 42, 36 | backgroundColor: Color.borderedMessageBackground 37 | ) 38 | } 39 | 40 | static var previews: some View { 41 | Group { 42 | enumeratingColorSchemes { 43 | reactionView 44 | .padding() 45 | .previewLayout(.sizeThatFits) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/MessageView/Reactions/ReactionsListItemView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | import NioKit 4 | 5 | struct ReactionsListItemView: View { 6 | @Environment(\.colorScheme) private var colorScheme: ColorScheme 7 | @Environment(\.colorSchemeContrast) private var colorSchemeContrast: ColorSchemeContrast 8 | @Environment(\.userId) private var userId 9 | 10 | let reaction: Reaction 11 | 12 | private var timestamp: String { 13 | Formatter.string( 14 | for: self.reaction.timestamp, 15 | dateStyle: .short, 16 | timeStyle: .short 17 | ) 18 | } 19 | 20 | var body: some View { 21 | HStack { 22 | Text(self.reaction.reaction) 23 | Text(self.reaction.sender) 24 | Spacer() 25 | Text(timestamp) 26 | .font(.footnote) 27 | } 28 | } 29 | } 30 | 31 | struct ReactionsListItemView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | Group { 34 | enumeratingColorSchemes { 35 | ReactionsListItemView( 36 | reaction: Reaction( 37 | id: "0", 38 | sender: "Jane Doe", 39 | timestamp: Date(), 40 | reaction: "❤️" 41 | ) 42 | ) 43 | .padding() 44 | } 45 | } 46 | .previewLayout(.sizeThatFits) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/RedactionEventView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct RedactionEventView: View { 4 | struct ViewModel { 5 | let sender: String 6 | let redactor: String 7 | let reason: String? 8 | } 9 | 10 | let model: ViewModel 11 | 12 | private var redactionText: String { 13 | if model.sender == model.redactor { 14 | return "🗑 \(L10n.Event.Redaction.redactSelf(model.redactor))" 15 | } 16 | return "🗑 \(L10n.Event.Redaction.redactOther(model.redactor, model.sender))" 17 | } 18 | 19 | var body: some View { 20 | HStack { 21 | Spacer() 22 | VStack { 23 | Text(redactionText) 24 | .font(.caption) 25 | .foregroundColor(.gray) 26 | if model.reason != nil { 27 | Text(verbatim: L10n.Event.reason(model.reason!)) 28 | .foregroundColor(.gray) 29 | .font(.callout) 30 | } 31 | } 32 | Spacer() 33 | } 34 | .padding(.vertical, 3) 35 | } 36 | } 37 | 38 | struct RedactionView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | Group { 41 | RedactionEventView(model: .init(sender: "Jane Doe", 42 | redactor: "Jane Doe", 43 | reason: nil)) 44 | .previewDisplayName("self redact") 45 | RedactionEventView(model: .init(sender: "John Doe", 46 | redactor: "Jane Doe", 47 | reason: nil)) 48 | .previewDisplayName("redact other") 49 | RedactionEventView(model: .init(sender: "Jane Doe", 50 | redactor: "Jane Doe", 51 | reason: "Totally valid reason with longer text")) 52 | .previewDisplayName("self redact with reason") 53 | 54 | VStack(spacing: 0) { 55 | RedactionEventView(model: .init(sender: "Jane Doe", 56 | redactor: "Jane Doe", 57 | reason: nil)) 58 | RedactionEventView(model: .init(sender: "Jane Doe", 59 | redactor: "Jane Doe", 60 | reason: "some reason")) 61 | RedactionEventView(model: .init(sender: "John Doe", 62 | redactor: "Jane Doe", 63 | reason: "spam spam spam")) 64 | } 65 | .previewDisplayName("spacing") 66 | } 67 | .padding() 68 | .previewLayout(.sizeThatFits) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/RoomNameEventView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import class MatrixSDK.MXEvent 3 | 4 | struct RoomNameEventView: View { 5 | struct ViewModel { 6 | fileprivate let sender: String 7 | fileprivate let newName: String 8 | fileprivate let oldName: String? 9 | 10 | init(event: MXEvent) { 11 | self.sender = event.sender ?? "" 12 | self.newName = event.content(valueFor: "name") ?? L10n.Event.unknownRoomNameFallback 13 | self.oldName = event.prevContent(valueFor: "name") 14 | } 15 | 16 | init(sender: String, newName: String, oldName: String?) { 17 | self.sender = sender 18 | self.newName = newName 19 | self.oldName = oldName 20 | } 21 | } 22 | 23 | let model: ViewModel 24 | 25 | var body: some View { 26 | if let oldName = model.oldName { 27 | return GenericEventView(text: L10n.Event.RoomName.changeName(model.sender, oldName, model.newName)) 28 | } 29 | return GenericEventView(text: L10n.Event.RoomName.setName(model.sender, model.newName)) 30 | } 31 | } 32 | 33 | struct RoomNameEventView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | VStack { 36 | RoomNameEventView(model: .init(sender: "Jane", newName: "New Room", oldName: nil)) 37 | RoomNameEventView(model: .init(sender: "Jane", newName: "New Room", oldName: "Old Room")) 38 | } 39 | .previewLayout(.sizeThatFits) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/RoomPowerLevelsEventView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import class MatrixSDK.MXEvent 3 | 4 | struct RoomPowerLevelsEventView: View { 5 | struct ViewModel { 6 | fileprivate let sender: String 7 | 8 | struct LevelChange: Identifiable { 9 | let name: String 10 | let old: Int? 11 | let new: Int 12 | 13 | var id: String { name } 14 | 15 | private func levelName(_ level: Int) -> String { 16 | switch level { 17 | case 0: return L10n.RoomPowerLevel.default 18 | case 50: return L10n.RoomPowerLevel.moderator 19 | case 100: return L10n.RoomPowerLevel.admin 20 | default: return L10n.RoomPowerLevel.custom(level) 21 | } 22 | } 23 | 24 | fileprivate var oldLevelName: String { 25 | old.map { levelName($0) } ?? L10n.RoomPowerLevel.default 26 | } 27 | 28 | fileprivate var newLevelName: String { 29 | levelName(new) 30 | } 31 | } 32 | private let changes: [LevelChange] 33 | 34 | init(sender: String, 35 | changes: [LevelChange]) { 36 | self.sender = sender 37 | self.changes = changes 38 | } 39 | 40 | init(event: MXEvent) { 41 | let previousUsers = (event.unsignedData?.prevContent?["users"] as? [String: Int]) ?? [:] 42 | let currentUsers = (event.content?["users"] as? [String: Int]) ?? [:] 43 | 44 | var changes: [LevelChange] = [] 45 | for (user, level) in currentUsers { 46 | if let oldLevel = previousUsers[user] { 47 | if oldLevel != level { 48 | changes.append(LevelChange(name: user, 49 | old: oldLevel, 50 | new: level)) 51 | } 52 | } else { 53 | changes.append(LevelChange(name: user, 54 | old: nil, 55 | new: level)) 56 | } 57 | } 58 | 59 | let sender = event.sender ?? "" 60 | self.init(sender: sender, changes: changes) 61 | } 62 | 63 | var combined: String { 64 | changes 65 | .map { 66 | if sender == $0.name { 67 | return L10n.Event.RoomPowerLevel.changeSelf(sender, $0.oldLevelName, $0.newLevelName) 68 | } 69 | return L10n.Event.RoomPowerLevel.changeOther(sender, $0.name, $0.oldLevelName, $0.newLevelName) 70 | } 71 | .joined(separator: "\n") 72 | } 73 | } 74 | 75 | let model: ViewModel 76 | 77 | var body: some View { 78 | GenericEventView(text: model.combined) 79 | } 80 | } 81 | 82 | struct RoomPowerLevelsEventView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | RoomPowerLevelsEventView(model: .init( 85 | sender: "Jane", 86 | changes: [ 87 | .init(name: "John", old: nil, new: 50), 88 | .init(name: "John", old: 50, new: 100), 89 | ])) 90 | .previewLayout(.sizeThatFits) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Nio/Conversations/Event Views/RoomTopicEventView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import class MatrixSDK.MXEvent 3 | 4 | struct RoomTopicEventView: View { 5 | struct ViewModel { 6 | let sender: String 7 | let topic: String 8 | 9 | init(sender: String, topic: String) { 10 | self.sender = sender 11 | self.topic = topic 12 | } 13 | 14 | init(event: MXEvent) { 15 | self.init(sender: event.sender ?? L10n.Event.unknownSenderFallback, 16 | topic: event.content(valueFor: "topic") ?? "") 17 | } 18 | } 19 | 20 | let model: ViewModel 21 | 22 | var body: some View { 23 | GenericEventView(text: L10n.Event.RoomTopic.change(model.sender, model.topic)) 24 | } 25 | } 26 | 27 | struct RoomTopicEventView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | RoomTopicEventView(model: .init(sender: "Jane Doe", topic: "The Orville")) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Nio/Conversations/TypingIndicatorView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | import NioKit 4 | 5 | struct TypingIndicatorContainerView: View { 6 | @EnvironmentObject private var room: NIORoom 7 | @Environment(\.userId) private var userId 8 | 9 | private var typingUsers: [String] { 10 | room.room.typingUsers?.filter { $0 != userId} ?? [] 11 | } 12 | 13 | var body: some View { 14 | TypingIndicatorView(typingUsers: typingUsers) 15 | } 16 | } 17 | 18 | struct TypingIndicatorView: View { 19 | @Environment(\.colorScheme) var colorScheme: ColorScheme 20 | let typingUsers: [String] 21 | 22 | private var text: String { 23 | switch typingUsers.count { 24 | case 1: 25 | return L10n.TypingIndicator.single(typingUsers.first!) 26 | case 2: 27 | return L10n.TypingIndicator.two(typingUsers[0], typingUsers[1]) 28 | default: 29 | return L10n.TypingIndicator.many 30 | } 31 | } 32 | 33 | private var backgroundColor: Color { 34 | switch self.colorScheme { 35 | case .dark: 36 | return Color(#colorLiteral(red: 0.1221848231, green: 0.1316168257, blue: 0.1457917546, alpha: 1)) 37 | default: 38 | return Color(#colorLiteral(red: 0.968541801, green: 0.9726034999, blue: 0.9763545394, alpha: 1)) 39 | } 40 | } 41 | 42 | var body: some View { 43 | HStack { 44 | Group { 45 | SFSymbol.typing 46 | Text(text) 47 | } 48 | .font(.caption) 49 | .foregroundColor(.gray) 50 | Spacer() 51 | } 52 | .padding(8) 53 | .background(self.backgroundColor) 54 | } 55 | } 56 | 57 | struct TypingIndicatorView_Previews: PreviewProvider { 58 | static var previews: some View { 59 | Group { 60 | TypingIndicatorView(typingUsers: ["Jane Doe"]) 61 | TypingIndicatorView(typingUsers: ["Jane Doe", "John Doe"]) 62 | TypingIndicatorView(typingUsers: ["Jane Doe", "John Doe", "Jill Doe"]) 63 | 64 | TypingIndicatorView(typingUsers: ["Jane Doe", "John Doe"]) 65 | .environment(\.colorScheme, .dark) 66 | 67 | TypingIndicatorView(typingUsers: ["Jane Doe", "John Doe"]) 68 | .environment(\.sizeCategory, .accessibilityLarge) 69 | } 70 | .previewLayout(.sizeThatFits) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Nio/Extensions/Color+Named.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | 4 | extension Color { 5 | static var borderedMessageBackground: Color = .init(Asset.Color.borderedMessageBackground.name) 6 | 7 | static func backgroundColor(for colorScheme: ColorScheme) -> Color { 8 | if colorScheme == .dark { 9 | return .black 10 | } else { 11 | return .white 12 | } 13 | } 14 | } 15 | 16 | extension UXColor { 17 | /// Color of text that is shown on top of the accent color, e.g. badges. 18 | static func textOnAccentColor(for colorScheme: ColorScheme) -> UXColor { 19 | messageTextColor(for: colorScheme, isOutgoing: true) 20 | } 21 | 22 | static func messageTextColor(for colorScheme: ColorScheme, 23 | isOutgoing: Bool) -> UXColor { 24 | if isOutgoing { 25 | return .white 26 | } 27 | switch colorScheme { 28 | case .dark: 29 | return .white 30 | default: 31 | return .black 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Nio/Extensions/Color+allAccent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Color: RawRepresentable { 4 | static var allAccentOptions: [Color] { 5 | [ 6 | .purple, 7 | .blue, 8 | .red, 9 | .orange, 10 | .green, 11 | .gray, 12 | .yellow 13 | ] 14 | } 15 | 16 | public init?(rawValue: String) { 17 | switch rawValue { 18 | case "purple": self = .purple 19 | case "blue": self = .blue 20 | case "red": self = .red 21 | case "orange": self = .orange 22 | case "green": self = .green 23 | case "gray": self = .gray 24 | case "yellow": self = .yellow 25 | default: self = .purple 26 | } 27 | } 28 | 29 | public var rawValue: String { description } 30 | } 31 | -------------------------------------------------------------------------------- /Nio/Extensions/ContentSizeCategory.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension ContentSizeCategory { 4 | var scalingFactor: CGFloat { 5 | switch self { 6 | case .extraSmall: 7 | return 0.8 8 | case .small: 9 | return 0.9 10 | case .medium: 11 | return 1.0 12 | case .large: 13 | return 1.1 14 | case .extraLarge: 15 | return 1.2 16 | case .extraExtraLarge: 17 | return 1.3 18 | case .extraExtraExtraLarge: 19 | return 1.4 20 | case .accessibilityMedium: 21 | return 1.5 22 | case .accessibilityLarge: 23 | return 1.6 24 | case .accessibilityExtraLarge: 25 | return 1.7 26 | case .accessibilityExtraExtraLarge: 27 | return 1.8 28 | case .accessibilityExtraExtraExtraLarge: 29 | return 1.9 30 | @unknown default: 31 | return 0.0 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Nio/Extensions/EnvironmentValues.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct UserIDKey: EnvironmentKey { 4 | static let defaultValue: String = "" 5 | } 6 | 7 | struct HomeserverKey: EnvironmentKey { 8 | static let defaultValue = URL(string: "https://matrix.org")! 9 | } 10 | 11 | extension EnvironmentValues { 12 | var userId: String { 13 | get { 14 | return self[UserIDKey.self] 15 | } 16 | set { 17 | self[UserIDKey.self] = newValue 18 | } 19 | } 20 | 21 | var homeserver: URL { 22 | get { 23 | return self[HomeserverKey.self] 24 | } 25 | set { 26 | self[HomeserverKey.self] = newValue 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Nio/Extensions/NSAttributedString+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSAttributedString { 4 | var isEmpty: Bool { 5 | self.length == 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Nio/Extensions/PreviewProvider+Enumeration.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension PreviewProvider { 4 | static func enumeratingColorSchemes( 5 | _ colorSchemes: [ColorScheme] = ColorScheme.allCases, 6 | _ content: @escaping () -> Content 7 | ) -> some View where Content: View { 8 | ForEach(colorSchemes, id: \.self) { colorScheme in 9 | Group { 10 | content() 11 | } 12 | .environment(\.colorScheme, colorScheme) 13 | .background(Color.backgroundColor(for: colorScheme)) 14 | } 15 | } 16 | 17 | static func enumeratingSizeCategories( 18 | _ sizeCategories: [ContentSizeCategory] = ContentSizeCategory.allCases, 19 | _ content: @escaping () -> Content 20 | ) -> some View where Content: View { 21 | Group { 22 | List { 23 | ForEach(sizeCategories, id: \.self) { sizeCategory in 24 | content().environment(\.sizeCategory, sizeCategory) 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Nio/Extensions/String+Emoji.swift: -------------------------------------------------------------------------------- 1 | // via https://stackoverflow.com/a/39425959/1843020 2 | 3 | extension Character { 4 | /// A simple emoji is one scalar and presented to the user as an Emoji 5 | var isSimpleEmoji: Bool { 6 | guard let firstProperties = unicodeScalars.first?.properties else { 7 | return false 8 | } 9 | return unicodeScalars.count == 1 && 10 | (firstProperties.isEmojiPresentation || 11 | firstProperties.generalCategory == .otherSymbol) 12 | } 13 | 14 | /// Checks if the scalars will be merged into an emoji 15 | var isCombinedIntoEmoji: Bool { 16 | return unicodeScalars.count > 1 && 17 | unicodeScalars.contains { $0.properties.isJoinControl || $0.properties.isVariationSelector } 18 | } 19 | 20 | var isEmoji: Bool { 21 | return isSimpleEmoji || isCombinedIntoEmoji 22 | } 23 | } 24 | 25 | extension String { 26 | var isSingleEmoji: Bool { 27 | return count == 1 && containsEmoji 28 | } 29 | 30 | var containsEmoji: Bool { 31 | return contains { $0.isEmoji } 32 | } 33 | 34 | var containsOnlyEmoji: Bool { 35 | return !isEmpty && !contains { !$0.isEmoji } 36 | } 37 | 38 | var emojiString: String { 39 | return emojis.map { String($0) }.reduce("", +) 40 | } 41 | 42 | var emojis: [Character] { 43 | return filter { $0.isEmoji } 44 | } 45 | 46 | var emojiScalars: [UnicodeScalar] { 47 | return filter { $0.isEmoji }.flatMap { $0.unicodeScalars } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Nio/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // FIXME: this seems like it could back-fire, 4 | // encouraging the use of stringly-typed code. 5 | extension String: Identifiable { 6 | public var id: String { 7 | self 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Nio/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppGroup 6 | $(APPGROUP) 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIcons 12 | 13 | CFBundleAlternateIcons 14 | 15 | Six Colors Dark 16 | 17 | CFBundleIconFiles 18 | 19 | Six Colors Dark 20 | 21 | UIPrerenderedIcon 22 | 23 | 24 | Six Colors Light 25 | 26 | CFBundleIconFiles 27 | 28 | Six Colors Light 29 | 30 | UIPrerenderedIcon 31 | 32 | 33 | Sketch 34 | 35 | CFBundleIconFiles 36 | 37 | Sketch 38 | 39 | UIPrerenderedIcon 40 | 41 | 42 | 43 | CFBundlePrimaryIcon 44 | 45 | CFBundleIconFiles 46 | 47 | AppIcon 48 | 49 | UIPrerenderedIcon 50 | 51 | 52 | 53 | CFBundleIdentifier 54 | $(PRODUCT_BUNDLE_IDENTIFIER) 55 | CFBundleInfoDictionaryVersion 56 | 6.0 57 | CFBundleName 58 | $(PRODUCT_NAME) 59 | CFBundlePackageType 60 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 61 | CFBundleShortVersionString 62 | $(MARKETING_VERSION) 63 | CFBundleVersion 64 | $(CURRENT_PROJECT_VERSION) 65 | LSRequiresIPhoneOS 66 | 67 | UIApplicationSceneManifest 68 | 69 | UIApplicationSupportsMultipleScenes 70 | 71 | 72 | UILaunchStoryboardName 73 | LaunchScreen 74 | UIRequiredDeviceCapabilities 75 | 76 | armv7 77 | 78 | UISupportedInterfaceOrientations 79 | 80 | UIInterfaceOrientationPortrait 81 | UIInterfaceOrientationLandscapeLeft 82 | UIInterfaceOrientationLandscapeRight 83 | 84 | DevelopmentTeam 85 | $(DEVELOPMENT_TEAM) 86 | UISupportedInterfaceOrientations~ipad 87 | 88 | UIInterfaceOrientationPortrait 89 | UIInterfaceOrientationPortraitUpsideDown 90 | UIInterfaceOrientationLandscapeLeft 91 | UIInterfaceOrientationLandscapeRight 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Nio/Nio.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.$(APPGROUP) 10 | 11 | com.apple.security.network.client 12 | 13 | keychain-access-groups 14 | 15 | $(AppIdentifierPrefix)nio.keychain 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Nio/NioApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | 4 | @main 5 | struct NioApp: App { 6 | @StateObject private var accountStore = AccountStore() 7 | 8 | @AppStorage("accentColor") private var accentColor: Color = .purple 9 | 10 | var body: some Scene { 11 | WindowGroup { 12 | #if os(macOS) 13 | RootView() 14 | .environmentObject(accountStore) 15 | .accentColor(accentColor) 16 | .frame(minWidth: Style.minWindowWidth, minHeight: Style.minWindowHeight) 17 | .presentedWindowToolbarStyle(UnifiedWindowToolbarStyle(showsTitle: false)) 18 | #else 19 | RootView() 20 | .environmentObject(accountStore) 21 | .accentColor(accentColor) 22 | #endif 23 | } 24 | 25 | #if os(macOS) 26 | Settings { 27 | SettingsContainerView() 28 | .environmentObject(accountStore) 29 | } 30 | #endif 31 | } 32 | } 33 | 34 | #if os(macOS) 35 | enum Style { 36 | static let minSidebarWidth = 280 as CGFloat 37 | static let minTimelineWidth = 480 as CGFloat 38 | static let minWindowWidth = minSidebarWidth + minTimelineWidth + 10 39 | static let minWindowHeight = 320 as CGFloat 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Nio/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Nio/Resources/Alternate Icons/Six Colors Dark/Six Colors Dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Resources/Alternate Icons/Six Colors Dark/Six Colors Dark@2x.png -------------------------------------------------------------------------------- /Nio/Resources/Alternate Icons/Six Colors Dark/Six Colors Dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Resources/Alternate Icons/Six Colors Dark/Six Colors Dark@3x.png -------------------------------------------------------------------------------- /Nio/Resources/Alternate Icons/Six Colors Light/Six Colors Light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Resources/Alternate Icons/Six Colors Light/Six Colors Light@2x.png -------------------------------------------------------------------------------- /Nio/Resources/Alternate Icons/Six Colors Light/Six Colors Light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Resources/Alternate Icons/Six Colors Light/Six Colors Light@3x.png -------------------------------------------------------------------------------- /Nio/Resources/Alternate Icons/Sketch/Sketch@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Resources/Alternate Icons/Sketch/Sketch@2x.png -------------------------------------------------------------------------------- /Nio/Resources/Alternate Icons/Sketch/Sketch@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Nio/Resources/Alternate Icons/Sketch/Sketch@3x.png -------------------------------------------------------------------------------- /Nio/RootView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | import NioKit 4 | 5 | struct RootView: View { 6 | @EnvironmentObject private var store: AccountStore 7 | 8 | private var homeserverURL: URL { 9 | // Can this ever be nil? And if so, what happens with the default fallback? 10 | assert(store.client != nil) 11 | let configuredURL = store.client?.homeserver.flatMap(URL.init) 12 | assert(configuredURL != nil) 13 | return configuredURL ?? HomeserverKey.defaultValue 14 | } 15 | 16 | var body: some View { 17 | switch store.loginState { 18 | case .loggedIn(let userId): 19 | RecentRoomsContainerView() 20 | .environment(\.userId, userId) 21 | .environment(\.homeserver, homeserverURL) 22 | 23 | case .loggedOut: 24 | LoginContainerView() 25 | 26 | case .authenticating: 27 | LoadingView() 28 | 29 | case .failure(let error): 30 | VStack { 31 | Spacer() 32 | Text(verbatim: error.localizedDescription) 33 | Spacer() 34 | Button(action: { self.store.loginState = .loggedOut }) { 35 | Text(verbatim: L10n.Login.failureBackToLogin) 36 | } 37 | .padding() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Nio/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | import NioKit 5 | // swiftlint:disable line_length 6 | 7 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 8 | 9 | var window: UIWindow? 10 | 11 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 12 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 13 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 14 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 15 | 16 | let accountStore = AccountStore() 17 | let appSettings = AppSettings() 18 | 19 | let rootView = RootView() 20 | .environmentObject(accountStore) 21 | .environmentObject(appSettings) 22 | .accentColor(appSettings.accentColor) 23 | 24 | // Use a UIHostingController as window root view controller. 25 | if let windowScene = scene as? UIWindowScene { 26 | let window = UIWindow(windowScene: windowScene) 27 | window.rootViewController = UIHostingController(rootView: rootView) 28 | self.window = window 29 | window.makeKeyAndVisible() 30 | } 31 | } 32 | 33 | func sceneDidDisconnect(_ scene: UIScene) { 34 | // Called as the scene is being released by the system. 35 | // This occurs shortly after the scene enters the background, or when its session is discarded. 36 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 37 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 38 | } 39 | 40 | func sceneDidBecomeActive(_ scene: UIScene) { 41 | // Called when the scene has moved from an inactive state to an active state. 42 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 43 | } 44 | 45 | func sceneWillResignActive(_ scene: UIScene) { 46 | // Called when the scene will move from an active state to an inactive state. 47 | // This may occur due to temporary interruptions (ex. an incoming phone call). 48 | } 49 | 50 | func sceneWillEnterForeground(_ scene: UIScene) { 51 | // Called as the scene transitions from the background to the foreground. 52 | // Use this method to undo the changes made on entering the background. 53 | } 54 | 55 | func sceneDidEnterBackground(_ scene: UIScene) { 56 | // Called as the scene transitions from the foreground to the background. 57 | // Use this method to save data, release shared resources, and store enough scene-specific state information 58 | // to restore the scene back to its current state. 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Nio/Settings/AppIcon.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AppIcon: Identifiable, View { 4 | let title: String 5 | let attribution: String? 6 | 7 | var id: String { 8 | title 9 | } 10 | 11 | var previewName: String { 12 | "App Icons/\(title)" 13 | } 14 | 15 | init(title: String, attribution: String? = nil) { 16 | self.title = title 17 | self.attribution = attribution 18 | } 19 | 20 | var body: some View { 21 | HStack { 22 | Image(previewName) 23 | .resizable() 24 | .frame(width: 60, height: 60) 25 | .cornerRadius(12) 26 | .padding(5) 27 | VStack(alignment: .leading) { 28 | Text(title) 29 | if attribution != nil { 30 | Text(attribution!) 31 | .font(.footnote) 32 | .foregroundColor(.gray) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | #if os(macOS) 40 | class AppIconTitle: ObservableObject { 41 | static var alternatives = [ "Default" ] 42 | 43 | var current: String { 44 | get { return Self.alternatives.first ?? "" } 45 | set { 46 | assert(newValue == Self.alternatives.first) 47 | } 48 | } 49 | } 50 | #else // iOS 51 | class AppIconTitle: ObservableObject { 52 | static var alternatives = [ 53 | "Default", 54 | "Six Colors Dark", 55 | "Six Colors Light", 56 | "Sketch", 57 | ] 58 | 59 | var current: String { 60 | get { 61 | UIApplication.shared.alternateIconName ?? "Default" 62 | } 63 | set { 64 | var iconName: String? = newValue 65 | if iconName == "Default" { 66 | iconName = nil 67 | } 68 | objectWillChange.send() 69 | UIApplication.shared.setAlternateIconName(iconName) { error in 70 | guard let error = error else { return } 71 | print("Error setting new app icon: \(error)") 72 | } 73 | } 74 | } 75 | } 76 | #endif // iOS 77 | -------------------------------------------------------------------------------- /Nio/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | 4 | struct SettingsContainerView: View { 5 | @EnvironmentObject var store: AccountStore 6 | 7 | var body: some View { 8 | #if os(macOS) 9 | MacSettingsView(logoutAction: self.store.logout) 10 | #else 11 | SettingsView(logoutAction: self.store.logout) 12 | #endif 13 | } 14 | } 15 | 16 | private struct MacSettingsView: View { 17 | @AppStorage("accentColor") private var accentColor: Color = .purple 18 | let logoutAction: () -> Void 19 | 20 | var body: some View { 21 | Form { 22 | Section { 23 | Picker(selection: $accentColor, label: Text(verbatim: L10n.Settings.accentColor)) { 24 | ForEach(Color.allAccentOptions, id: \.self) { color in 25 | HStack { 26 | Circle() 27 | .frame(width: 20) 28 | .foregroundColor(color) 29 | Text(color.description.capitalized) 30 | } 31 | .tag(color) 32 | } 33 | } 34 | // No icon picker on macOS 35 | } 36 | 37 | Section { 38 | Button(action: self.logoutAction) { 39 | Text(verbatim: L10n.Settings.logOut) 40 | } 41 | } 42 | } 43 | .padding() 44 | .frame(maxWidth: 320) 45 | } 46 | } 47 | 48 | private struct SettingsView: View { 49 | @AppStorage("accentColor") private var accentColor: Color = .purple 50 | @StateObject private var appIconTitle = AppIconTitle() 51 | let logoutAction: () -> Void 52 | 53 | @Environment(\.presentationMode) private var presentationMode 54 | 55 | var body: some View { 56 | NavigationView { 57 | Form { 58 | Section { 59 | Picker(selection: $accentColor, label: Text(verbatim: L10n.Settings.accentColor)) { 60 | ForEach(Color.allAccentOptions, id: \.self) { color in 61 | HStack { 62 | Circle() 63 | .frame(width: 20) 64 | .foregroundColor(color) 65 | Text(color.description.capitalized) 66 | } 67 | .tag(color) 68 | } 69 | } 70 | 71 | Picker(selection: $appIconTitle.current, label: Text(verbatim: L10n.Settings.appIcon)) { 72 | ForEach(AppIconTitle.alternatives) { AppIcon(title: $0) } 73 | } 74 | } 75 | 76 | Section { 77 | Button(action: self.logoutAction) { 78 | Text(verbatim: L10n.Settings.logOut) 79 | } 80 | } 81 | } 82 | .navigationBarTitle(L10n.Settings.title, displayMode: .inline) 83 | .toolbar { 84 | ToolbarItem(placement: .confirmationAction) { 85 | Button(L10n.Settings.dismiss) { 86 | presentationMode.wrappedValue.dismiss() 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | struct SettingsView_Previews: PreviewProvider { 95 | static var previews: some View { 96 | SettingsView(logoutAction: {}) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Nio/Shapes/IndividuallyRoundedRectangle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct IndividuallyRoundedRectangle: Shape { 4 | var topLeft: CGFloat = 0.0 5 | var topRight: CGFloat = 0.0 6 | var bottomLeft: CGFloat = 0.0 7 | var bottomRight: CGFloat = 0.0 8 | 9 | func path(in rect: CGRect) -> Path { 10 | var path = Path() 11 | 12 | let width = rect.size.width 13 | let height = rect.size.height 14 | 15 | let topRight = min(min(self.topRight, height * 0.5), width * 0.5) 16 | let topLeft = min(min(self.topLeft, height * 0.5), width * 0.5) 17 | let bottomLeft = min(min(self.bottomLeft, height * 0.5), width * 0.5) 18 | let bottomRight = min(min(self.bottomRight, height * 0.5), width * 0.5) 19 | 20 | path.move(to: CGPoint(x: width * 0.5, y: 0.0)) 21 | 22 | path.addLine(to: CGPoint(x: width - topRight, y: 0.0)) 23 | path.addArc( 24 | center: CGPoint(x: width - topRight, y: topRight), 25 | radius: topRight, 26 | startAngle: Angle(degrees: -90.0), 27 | endAngle: Angle(degrees: 0.0), 28 | clockwise: false 29 | ) 30 | 31 | path.addLine(to: CGPoint(x: width, y: height - bottomRight)) 32 | path.addArc( 33 | center: CGPoint(x: width - bottomRight, y: height - bottomRight), 34 | radius: bottomRight, 35 | startAngle: Angle(degrees: 0.0), 36 | endAngle: Angle(degrees: 90.0), 37 | clockwise: false 38 | ) 39 | 40 | path.addLine(to: CGPoint(x: bottomLeft, y: height)) 41 | path.addArc( 42 | center: CGPoint(x: bottomLeft, y: height - bottomLeft), 43 | radius: bottomLeft, 44 | startAngle: Angle(degrees: 90.0), 45 | endAngle: Angle(degrees: 180.0), 46 | clockwise: false 47 | ) 48 | 49 | path.addLine(to: CGPoint(x: 0.0, y: topLeft)) 50 | path.addArc( 51 | center: CGPoint(x: topLeft, y: topLeft), 52 | radius: topLeft, 53 | startAngle: Angle(degrees: 180.0), 54 | endAngle: Angle(degrees: 270.0), 55 | clockwise: false 56 | ) 57 | 58 | return path 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Nio/Shared Views/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(macOS) 4 | 5 | struct ImagePicker: View { 6 | 7 | var body: some View { 8 | Text("Sorrz, no image picker on macOS yet :-/") 9 | } 10 | } 11 | 12 | #else // iOS 13 | struct ImagePicker: UIViewControllerRepresentable { 14 | 15 | @Environment(\.presentationMode) 16 | private var presentationMode 17 | 18 | let sourceType: UIImagePickerController.SourceType 19 | let onImagePicked: (UIImage) -> Void 20 | 21 | final class Coordinator: NSObject, 22 | UINavigationControllerDelegate, 23 | UIImagePickerControllerDelegate { 24 | 25 | @Binding 26 | private var presentationMode: PresentationMode 27 | private let sourceType: UIImagePickerController.SourceType 28 | private let onImagePicked: (UIImage) -> Void 29 | 30 | init(presentationMode: Binding, 31 | sourceType: UIImagePickerController.SourceType, 32 | onImagePicked: @escaping (UIImage) -> Void) { 33 | _presentationMode = presentationMode 34 | self.sourceType = sourceType 35 | self.onImagePicked = onImagePicked 36 | } 37 | 38 | func imagePickerController(_ picker: UIImagePickerController, 39 | didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { 40 | // swiftlint:disable force_cast 41 | let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage 42 | // swiftlint:enable force_cast 43 | onImagePicked(uiImage) 44 | presentationMode.dismiss() 45 | 46 | } 47 | 48 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 49 | presentationMode.dismiss() 50 | } 51 | 52 | } 53 | 54 | func makeCoordinator() -> Coordinator { 55 | return Coordinator(presentationMode: presentationMode, 56 | sourceType: sourceType, 57 | onImagePicked: onImagePicked) 58 | } 59 | 60 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { 61 | let picker = UIImagePickerController() 62 | picker.sourceType = sourceType 63 | picker.delegate = context.coordinator 64 | return picker 65 | } 66 | 67 | func updateUIViewController(_ uiViewController: UIImagePickerController, 68 | context: UIViewControllerRepresentableContext) { 69 | 70 | } 71 | 72 | } 73 | #endif // iOS 74 | -------------------------------------------------------------------------------- /Nio/Shared Views/MessageTextViewWrapper.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | 4 | #if os(macOS) 5 | class MessageTextView: NSTextView { 6 | convenience init(attributedString: NSAttributedString, linkColor: UXColor, 7 | maxSize: CGSize) 8 | { 9 | self.init() 10 | backgroundColor = .clear 11 | textContainerInset = .zero 12 | isEditable = false 13 | linkTextAttributes = [ 14 | .foregroundColor: linkColor, 15 | .underlineStyle: NSUnderlineStyle.single.rawValue, 16 | ] 17 | 18 | self.insertText(attributedString, 19 | replacementRange: NSRange(location: 0, length: 0)) 20 | self.maxSize = maxSize 21 | 22 | // don't resist text wrapping across multiple lines 23 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 24 | } 25 | } 26 | 27 | struct MessageTextViewWrapper: NSViewRepresentable { 28 | let attributedString: NSAttributedString 29 | let linkColor: NSColor 30 | let maxSize: CGSize 31 | 32 | func makeNSView(context: Context) -> MessageTextView { 33 | MessageTextView(attributedString: attributedString, linkColor: linkColor, maxSize: maxSize) 34 | } 35 | 36 | func updateNSView(_ uiView: MessageTextView, context: Context) { 37 | // nothing to update 38 | } 39 | 40 | func makeCoordinator() { 41 | // nothing to coordinate 42 | } 43 | } 44 | #else // iOS 45 | /// An automatically sized label, which allows links to be tapped. 46 | class MessageTextView: UITextView { 47 | var maxSize: CGSize = .zero 48 | 49 | // Allows SwiftUI to automatically size the label appropriately 50 | override var intrinsicContentSize: CGSize { 51 | sizeThatFits(CGSize(width: maxSize.width, height: .infinity)) 52 | } 53 | 54 | convenience init(attributedString: NSAttributedString, linkColor: UIColor, maxSize: CGSize) { 55 | self.init() 56 | 57 | font = UIFont.preferredFont(forTextStyle: .body) 58 | textColor = UIColor.label 59 | backgroundColor = .clear 60 | textContainerInset = .zero 61 | textContainer.lineFragmentPadding = 0 62 | dataDetectorTypes = .all 63 | isEditable = false 64 | isScrollEnabled = false 65 | linkTextAttributes = [ 66 | .foregroundColor: linkColor, 67 | .underlineStyle: NSUnderlineStyle.single.rawValue, 68 | ] 69 | 70 | attributedText = attributedString 71 | self.maxSize = maxSize 72 | 73 | // don't resist text wrapping across multiple lines 74 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 75 | } 76 | } 77 | 78 | struct MessageTextViewWrapper: UIViewRepresentable { 79 | let attributedString: NSAttributedString 80 | let linkColor: UIColor 81 | let maxSize: CGSize 82 | 83 | func makeUIView(context: Context) -> MessageTextView { 84 | MessageTextView(attributedString: attributedString, linkColor: linkColor, maxSize: maxSize) 85 | } 86 | 87 | func updateUIView(_ uiView: MessageTextView, context: Context) { 88 | // nothing to update 89 | } 90 | 91 | func makeCoordinator() { 92 | // nothing to coordinate 93 | } 94 | } 95 | #endif // iOS 96 | -------------------------------------------------------------------------------- /Nio/Shared Views/MultilineTextField.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | 4 | struct MultilineTextField: View { 5 | @Binding private var attributedText: NSAttributedString 6 | @Binding private var isEditing: Bool 7 | 8 | @State private var contentSizeThatFits: CGSize = .zero 9 | 10 | private let placeholder: String 11 | private let textAttributes: TextAttributes 12 | 13 | private let onEditingChanged: ((Bool) -> Void)? 14 | private let onCommit: (() -> Void)? 15 | 16 | private var placeholderInset: EdgeInsets { 17 | .init(top: 8.0, leading: 8.0, bottom: 8.0, trailing: 8.0) 18 | } 19 | 20 | private var textContainerInset: UXEdgeInsets { 21 | .init(top: 8.0, left: 0.0, bottom: 8.0, right: 0.0) 22 | } 23 | 24 | private var lineFragmentPadding: CGFloat { 25 | 8.0 26 | } 27 | 28 | init ( 29 | attributedText: Binding, 30 | placeholder: String = "", 31 | isEditing: Binding, 32 | textAttributes: TextAttributes = .init(), 33 | onEditingChanged: ((Bool) -> Void)? = nil, 34 | onCommit: (() -> Void)? = nil 35 | ) { 36 | self._attributedText = attributedText 37 | self.placeholder = placeholder 38 | 39 | self._isEditing = isEditing 40 | 41 | self._contentSizeThatFits = State(initialValue: .zero) 42 | 43 | self.textAttributes = textAttributes 44 | 45 | self.onEditingChanged = onEditingChanged 46 | self.onCommit = onCommit 47 | } 48 | 49 | #if os(macOS) 50 | var plainStringBinding : Binding { 51 | return .init(get: { attributedText.string }, 52 | set: { attributedText = NSAttributedString(string: $0) }) 53 | } 54 | #endif 55 | 56 | var body: some View { 57 | #if os(macOS) 58 | TextField("Compose message", 59 | text: plainStringBinding, 60 | onEditingChanged: onEditingChanged ?? { _ in }, 61 | onCommit: onCommit ?? {}) 62 | .textFieldStyle(RoundedBorderTextFieldStyle()) 63 | #else // iOS 64 | AttributedText( 65 | attributedText: $attributedText, 66 | isEditing: $isEditing, 67 | textAttributes: textAttributes, 68 | onEditingChanged: onEditingChanged, 69 | onCommit: onCommit 70 | ) 71 | .onPreferenceChange(ContentSizeThatFitsKey.self) { 72 | self.contentSizeThatFits = $0 73 | } 74 | .frame( 75 | idealHeight: self.contentSizeThatFits.height 76 | ) 77 | .background(placeholderView, alignment: .topLeading) 78 | #endif // iOS 79 | } 80 | 81 | @ViewBuilder private var placeholderView: some View { 82 | if attributedText.isEmpty { 83 | Text(placeholder).foregroundColor(.gray) 84 | .padding(placeholderInset) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Nio/Shared Views/ReverseList.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct IsVisibleKey: PreferenceKey { 4 | static var defaultValue: Bool = false 5 | 6 | static func reduce(value: inout Bool, nextValue: () -> Bool) { 7 | value = nextValue() 8 | } 9 | } 10 | 11 | struct ReverseList: View where Element: Identifiable, Content: View { 12 | private let items: [Element] 13 | private let reverseItemOrder: Bool 14 | private let viewForItem: (Element) -> Content 15 | 16 | @Binding var hasReachedTop: Bool 17 | 18 | init(_ items: [Element], reverseItemOrder: Bool = true, hasReachedTop: Binding, viewForItem: @escaping (Element) -> Content) { 19 | self.items = items 20 | self.reverseItemOrder = reverseItemOrder 21 | self._hasReachedTop = hasReachedTop 22 | self.viewForItem = viewForItem 23 | } 24 | 25 | var body: some View { 26 | GeometryReader { contentsGeometry in 27 | ScrollView { 28 | ForEach(reverseItemOrder ? items.reversed() : items) { item in 29 | self.viewForItem(item) 30 | .scaleEffect(x: -1.0, y: 1.0) 31 | .rotationEffect(.degrees(180)) 32 | } 33 | GeometryReader { topViewGeometry in 34 | let frame = topViewGeometry.frame(in: .global) 35 | let isVisible = contentsGeometry.frame(in: .global).contains(CGPoint(x: frame.midX, y: frame.midY)) 36 | 37 | HStack { 38 | Spacer() 39 | ProgressView().progressViewStyle(CircularProgressViewStyle()) 40 | Spacer() 41 | } 42 | .preference(key: IsVisibleKey.self, value: isVisible) 43 | } 44 | .frame(height: 30) // FIXME: Frame height shouldn't be hard-coded 45 | .onPreferenceChange(IsVisibleKey.self) { 46 | hasReachedTop = $0 47 | } 48 | } 49 | .scaleEffect(x: -1.0, y: 1.0) 50 | .rotationEffect(.degrees(180)) 51 | } 52 | } 53 | } 54 | 55 | struct ReverseList_Previews: PreviewProvider { 56 | static var previews: some View { 57 | ReverseList(["1", "2", "3"], hasReachedTop: .constant(false)) { 58 | Text($0) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Nio/Shared Views/SFSymbol.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum SFSymbol: String, View { 4 | case typing = "scribble.variable" 5 | case close = "xmark" 6 | 7 | case newConversation = "square.and.pencil" 8 | case settings = "gear" 9 | case send = "paperplane" 10 | case attach = "paperclip" 11 | 12 | var body: some View { 13 | Image(systemName: self.rawValue) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Nio/Supporting Files/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /Nio/Supporting Files/bg.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | "login.open-registration-prompt" = "Все още нямате профил?"; 5 | "login.sign-in" = "Вписване"; 6 | "login.form.homeserver-optional-explanation" = "Домашния сървър не е задължителен, ако ползвате matrix.org."; 7 | "login.form.homeserver" = "Домашен сървър"; 8 | "login.form.password" = "Парола"; 9 | "login.form.username" = "Потребителско име"; 10 | "login.welcome-message" = "Влезте в профила си, за да започнете."; 11 | "login.welcome-header" = "Добре дошли в "; 12 | "recent-rooms.leave.alert-title" = "Напускане на стая"; 13 | "recent-rooms.rooms.header" = "Стаи"; 14 | "recent-rooms.accessibility-label.new-conversation" = "Нов разговор"; 15 | "recent-rooms.new-message-placeholder" = "Ново съобщение"; 16 | "recent-rooms.accessibility-label.settings" = "Настройки"; 17 | "recent-rooms.pending-invitations.leave.alert-title" = "Отхвърляне на поканата?"; 18 | "recent-rooms.accessibility-label.new-message-badge" = "%u нови съобщения"; 19 | // Room $NAME, $LAST_ACTIVITY_TIMESTAMP $LAST_MESSAGE 20 | "recent-rooms.accessibility-label.room" = "Стая %@, %@ %@"; 21 | "loading.cancel" = "Отмени"; 22 | "loading.4" = "Възстановяване на сесията"; 23 | "loading.3" = "Вписване"; 24 | "login.failure-back-to-login" = "Обратно към Впиши се"; 25 | "login.register-not-yet-implemented" = "Регистрацията на нов акаунт все още не е налична."; 26 | -------------------------------------------------------------------------------- /Nio/Supporting Files/fi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | "new-conversation.done" = "Valmis"; 4 | "new-conversation.edit" = "Muokkaa"; 5 | "new-conversation.cancel" = "Peruuta"; 6 | "new-conversation.alert-failed" = "Keskustelun aloittaminen epäonnistui"; 7 | "new-conversation.create-room" = "Aloita keskustelu"; 8 | "new-conversation.public-room" = "Julkinen huone"; 9 | "new-conversation.room-name" = "Huoneen nimi"; 10 | "new-conversation.title-room" = "Uusi huone"; 11 | "new-conversation.title-chat" = "Uusi keskustelu"; 12 | "settings.dismiss" = "Valmis"; 13 | "settings.log-out" = "Kirjaudu ulos"; 14 | "settings.app-icon" = "Sovelluksen kuvake"; 15 | "settings.accent-color" = "Korostusväri"; 16 | "reaction-picker.title" = "Napauta emojia lähettääksesi reaktion."; 17 | "event.context-menu.remove" = "Poista"; 18 | "event.context-menu.edit" = "Muokkaa"; 19 | "event.context-menu.reply" = "Vastaa"; 20 | "event.context-menu.add-reaction" = "Lisää reaktio"; 21 | "settings.title" = "Asetukset"; 22 | "room-power-level.default" = "Oletus"; 23 | "event.room-topic.change" = "%@ muutti aiheeksi '%@'"; 24 | "event.room-name.change-name" = "%@ muutti huoneen %@ uudeksi nimeksi %@"; 25 | "event.room-name.set-name" = "%@ asetti huoneen nimeksi %@"; 26 | "event.reason" = "Syy: %@"; 27 | "event.room-member.remove-avatar" = "%@ poisti profiilikuvansa"; 28 | "event.room-member.change-avatar" = "%@ päivitti profiilikuvansa"; 29 | "event.room-member.set-avatar" = "%@ asetti profiilikuvansa"; 30 | "event.room-member.change-name" = "%@ vaihtoi näyttönimekseen %@"; 31 | "event.room-member.joined" = "%@ liittyi"; 32 | "event.room-member.ban" = "%@ esti käyttäjän %@"; 33 | "event.room-member.kicked" = "%@ potki käyttäjän %@"; 34 | "event.room-member.left" = "%@ poistui"; 35 | "event.room-member.invited" = "%@ kutsui käyttäjän %@"; 36 | "event.unknown-room-name-fallback" = "Tuntematon"; 37 | "event.unknown-sender-fallback" = "Tuntematon"; 38 | "typing-indicator.many" = "Useampi ihminen kirjoittaa"; 39 | "typing-indicator.two" = "%@ ja %@ kirjoittavat"; 40 | "typing-indicator.single" = "%@ kirjoittaa"; 41 | "composer.accessibility-label.cancelEdit" = "Peruuta"; 42 | "composer.accessibility-label.send" = "Lähetä"; 43 | "composer.accessibility-label.send-file" = "Lähetä tiedosto"; 44 | "composer.edit-message" = "Muokkaa viestiä:"; 45 | "composer.new-message" = "Uusi viesti…"; 46 | "room.invitation.join-alert.join-button" = "Liity"; 47 | "room.invitation.join-alert.message" = "Hyväksytäänkö kutsu liittyä '%@'?"; 48 | "room.invitation.join-alert.title" = "Liitytäänkö keskusteluun?"; 49 | "room.invitation.fallback-title" = "Uusi keskustelu"; 50 | "room.remove.action" = "Poista"; 51 | "room.remove.message" = "Haluatko varmasti poistaa tämän viestin?"; 52 | "room.remove.title" = "Poistetaanko?"; 53 | "room.attachment.type-photo" = "Kuva"; 54 | "room.attachment.select-type" = "Lähetä liite"; 55 | "recent-rooms.leave.alert-title" = "Poistu huoneesta"; 56 | "recent-rooms.rooms.header" = "Huoneet"; 57 | "recent-rooms.pending-invitations.leave.alert-title" = "Hylätäänkö kutsu?"; 58 | "recent-rooms.pending-invitations.header" = "Odottavat kutsut"; 59 | "recent-rooms.accessibility-label.new-message-badge" = "%u uutta viestiä"; 60 | // Room $NAME, $LAST_ACTIVITY_TIMESTAMP $LAST_MESSAGE 61 | "recent-rooms.accessibility-label.room" = "Huone %@, %@ %@"; 62 | "recent-rooms.new-message-placeholder" = "Uusi viesti"; 63 | "recent-rooms.accessibility-label.new-conversation" = "Uusi keskustelu"; 64 | "recent-rooms.accessibility-label.settings" = "Asetukset"; 65 | "loading.cancel" = "Peruuta"; 66 | "loading.4" = "Palautetaan istunto"; 67 | "loading.3" = "Kirjaudutaan sisään"; 68 | "login.failure-back-to-login" = "Takaisin kirjautumiseen"; 69 | "login.register-not-yet-implemented" = "Uusien tilien rekisteröintiä ei ole vielä toteutettu."; 70 | "login.open-registration-prompt" = "Vailla tiliä?"; 71 | "login.sign-in" = "Kirjaudu sisään"; 72 | "login.form.homeserver-optional-explanation" = "Kotipalvelin on valinnainen, jos käytät matrix.orgia."; 73 | "login.form.homeserver" = "Kotipalvelin"; 74 | "login.form.password" = "Salasana"; 75 | "login.form.username" = "Käyttäjänimi"; 76 | "login.welcome-message" = "Kirjaudu tilillesi aloittaaksesi."; 77 | "login.welcome-header" = "Tervetuloa, tämä on "; 78 | -------------------------------------------------------------------------------- /Nio/Supporting Files/ko.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Nio/Supporting Files/lt.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | "login.form.username" = "Naudotojo vardas"; 4 | "login.form.password" = "Slaptažodis"; 5 | "login.open-registration-prompt" = "Dar neturite paskyros?"; 6 | "login.failure-back-to-login" = "Atgal į prisijungimą"; 7 | "recent-rooms.accessibility-label.new-conversation" = "Naujas pokalbis"; 8 | "recent-rooms.new-message-placeholder" = "Nauja žinutė"; 9 | "recent-rooms.accessibility-label.new-message-badge" = "Naujų žinučių: %u"; 10 | "login.welcome-header" = "Sveiki atvykę į "; 11 | "login.welcome-message" = "Norėdami pradėti, prisijunkite prie savo paskyros."; 12 | "login.sign-in" = "Prisijungti"; 13 | "login.register-not-yet-implemented" = "Naujų paskyrų registracija kol kas neįgyvendinta."; 14 | "loading.cancel" = "Atsisakyti"; 15 | "recent-rooms.accessibility-label.settings" = "Nustatymai"; 16 | "composer.accessibility-label.send" = "Siųsti"; 17 | "composer.accessibility-label.send-file" = "Siųsti failą"; 18 | "composer.edit-message" = "Redaguoti žinutę:"; 19 | "composer.new-message" = "Nauja žinutė…"; 20 | "room.invitation.join-alert.message" = "Priimti kvietimą prisijungti prie '%@'?"; 21 | "room.invitation.join-alert.title" = "Prisijungti prie pokalbio?"; 22 | "room.invitation.fallback-title" = "Naujas pokalbis"; 23 | "room.remove.action" = "Pašalinti"; 24 | "room.attachment.type-photo" = "Nuotrauka"; 25 | "recent-rooms.rooms.header" = "Kambariai"; 26 | "room.remove.title" = "Pašalinti?"; 27 | "room.invitation.join-alert.join-button" = "Prisijungti"; 28 | "recent-rooms.leave.alert-title" = "Palikti kambarį"; 29 | "login.form.homeserver" = "Namų serveris"; 30 | -------------------------------------------------------------------------------- /Nio/Supporting Files/sq.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Nio/Supporting Files/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "login.welcome-header" = "欢迎来到 "; 2 | "login.welcome-message" = "登录账户来开始使用。"; 3 | "login.form.username" = "用户名"; 4 | "login.form.password" = "密码"; 5 | "login.form.homeserver" = "主服务器"; 6 | "login.form.homeserver-optional-explanation" = "若是matrix.org,则不必填写主服务器。"; 7 | "login.sign-in" = "登录"; 8 | "login.open-registration-prompt" = "还没有账户?"; 9 | "login.register-not-yet-implemented" = "新账户注册功能尚未被实现。"; 10 | "login.failure-back-to-login" = "返回登录页面"; 11 | "loading.1" = "少女祈祷中"; 12 | "loading.2" = "正在给长者+1s"; 13 | "loading.3" = "登录中"; 14 | "loading.4" = "正在恢复会话"; 15 | "loading.cancel" = "取消"; 16 | "recent-rooms.accessibility-label.settings" = "设置"; 17 | "recent-rooms.accessibility-label.new-conversation" = "新对话"; 18 | "recent-rooms.new-message-placeholder" = "新信息"; 19 | /*DM with $NAME, $LAST_ACTIVITY_TIMESTAMP $LAST_MESSAGE*/ 20 | "recent-rooms.accessibility-label.dm" = "私聊%@, %@ %@"; 21 | /*Room $NAME, $LAST_ACTIVITY_TIMESTAMP $LAST_MESSAGE*/ 22 | "recent-rooms.accessibility-label.room" = "房间%@, %@ %@"; 23 | "recent-rooms.accessibility-label.new-message-badge" = "%u条新信息"; 24 | "recent-rooms.pending-invitations.header" = "待决邀请"; 25 | "recent-rooms.pending-invitations.leave.alert-title" = "拒绝邀请?"; 26 | "recent-rooms.rooms.header" = "房间"; 27 | "recent-rooms.leave.alert-title" = "离开房间"; 28 | "recent-rooms.leave.alert-body" = "你确定要离开\"%@\"吗?这个操作是不可逆的。"; 29 | "room.attachment.select-type" = "发送附件"; 30 | "room.attachment.type-photo" = "照片"; 31 | "room.remove.title" = "删除?"; 32 | "room.remove.message" = "你确定要删除这条消息吗?"; 33 | "room.remove.action" = "删除"; 34 | "room.invitation.fallback-title" = "新对话"; 35 | "room.invitation.join-alert.title" = "加入对话?"; 36 | "room.invitation.join-alert.message" = "接受加入'%@'的邀请?"; 37 | "room.invitation.join-alert.join-button" = "加入"; 38 | "composer.new-message" = "新信息…"; 39 | "composer.edit-message" = "编辑信息:"; 40 | "composer.accessibility-label.send-file" = "发送文件"; 41 | "composer.accessibility-label.send" = "发送"; 42 | "composer.accessibility-label.cancelEdit" = "取消"; 43 | "typing-indicator.single" = "%@正在输入"; 44 | "typing-indicator.two" = "%@和%@正在输入"; 45 | "typing-indicator.many" = "多位正在输入"; 46 | "event.unknown-sender-fallback" = "未知发送者"; 47 | "event.unknown-room-name-fallback" = "未知房间名"; 48 | "event.room-member.invited" = "%@邀请了%@"; 49 | "event.room-member.left" = "%@离开了房间"; 50 | "event.room-member.kicked" = "%@踢出了%@"; 51 | "event.room-member.ban" = "%@封禁了%@"; 52 | "event.room-member.joined" = "%@加入了房间"; 53 | "event.room-member.change-name" = "%@将其显示名称改为了%@"; 54 | "event.room-member.set-avatar" = "%@设置了头像"; 55 | "event.room-member.change-avatar" = "%@更新了头像"; 56 | "event.room-member.remove-avatar" = "%@移除了头像"; 57 | "event.room-member.unknown-state" = "未知成员事件状态:%@"; 58 | "event.reason" = "原因:%@"; 59 | "event.room-name.set-name" = "%@将房间名设为了%@"; 60 | "event.room-name.change-name" = "%@将房间名由%@改为了%@"; 61 | "event.room-topic.change" = "%@将主题改为了'%@'"; 62 | "room-power-level.default" = "默认"; 63 | "room-power-level.moderator" = "协管员"; 64 | "room-power-level.admin" = "管理员"; 65 | "room-power-level.custom" = "自定义(%d)"; 66 | "event.room-power-level.change-self" = "%@将其权力等级由%@改为了%@"; 67 | "event.room-power-level.change-other" = "%@将%@的权力等级由%@改为了%@"; 68 | "event.redaction.redact-self" = "被%@删除的消息"; 69 | "event.redaction.redact-other" = "%@删除了%@的消息"; 70 | "event.edited" = "已编辑"; 71 | "event.context-menu.add-reaction" = "添加反应"; 72 | "event.context-menu.reply" = "回复"; 73 | "event.context-menu.edit" = "编辑"; 74 | "event.context-menu.remove" = "删除"; 75 | "reaction-picker.title" = "点击表情以发送反应。"; 76 | "settings.title" = "设置"; 77 | "settings.accent-color" = "主颜色"; 78 | "settings.app-icon" = "应用图标"; 79 | "settings.log-out" = "注销"; 80 | "settings.dismiss" = "完成"; 81 | "new-conversation.username-placeholder" = "Matrix ID"; 82 | "new-conversation.cancel" = "取消"; 83 | "new-conversation.alert-failed" = "未能开始聊天"; 84 | "new-conversation.create-room" = "开始聊天"; 85 | "new-conversation.public-room" = "公共房间"; 86 | "new-conversation.room-name" = "房间名"; 87 | "new-conversation.for-example" = "例如"; 88 | "new-conversation.title-room" = "新房间"; 89 | "new-conversation.title-chat" = "新聊天"; 90 | "new-conversation.done" = "完成"; 91 | "new-conversation.edit" = "编辑"; 92 | -------------------------------------------------------------------------------- /Nio/Supporting Files/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | 3 | "event.room-member.joined" = "%@ 已加入"; 4 | "event.room-member.ban" = "%@封禁了%@"; 5 | "event.room-member.kicked" = "%@將%@踢出"; 6 | "event.room-member.left" = "%@ 已離開"; 7 | "event.room-member.invited" = "%@邀請了%@"; 8 | "event.unknown-room-name-fallback" = "未知傳送人"; 9 | "event.unknown-sender-fallback" = "未知聊天室"; 10 | "typing-indicator.many" = "有幾位正在打字"; 11 | "typing-indicator.two" = "%@ 和 %@ 正在打字"; 12 | "typing-indicator.single" = "%@ 正在打字"; 13 | "composer.accessibility-label.cancelEdit" = "取消"; 14 | "composer.accessibility-label.send" = "傳送"; 15 | "composer.accessibility-label.send-file" = "傳送檔案"; 16 | "composer.edit-message" = "編輯訊息:"; 17 | "composer.new-message" = "新訊息…"; 18 | "room.invitation.join-alert.join-button" = "加入"; 19 | "room.invitation.join-alert.message" = "接受邀請加入「%@」?"; 20 | "room.invitation.join-alert.title" = "加入對話?"; 21 | "room.invitation.fallback-title" = "新對話"; 22 | "room.remove.action" = "移除"; 23 | "room.remove.message" = "您確定要刪除這則訊息嗎?"; 24 | "room.remove.title" = "移除?"; 25 | "room.attachment.type-photo" = "照片"; 26 | "room.attachment.select-type" = "傳送附件"; 27 | "recent-rooms.leave.alert-body" = "確實要離開「%@」嗎?此動作無法復原。"; 28 | "recent-rooms.leave.alert-title" = "離開聊天室"; 29 | "recent-rooms.rooms.header" = "聊天室"; 30 | "recent-rooms.pending-invitations.leave.alert-title" = "拒絕邀請?"; 31 | "recent-rooms.pending-invitations.header" = "待處理的邀請"; 32 | "recent-rooms.accessibility-label.new-message-badge" = "%u 則新訊息"; 33 | // Room $NAME, $LAST_ACTIVITY_TIMESTAMP $LAST_MESSAGE 34 | "recent-rooms.accessibility-label.room" = "%@聊天室,%@ %@"; 35 | // DM with $NAME, $LAST_ACTIVITY_TIMESTAMP $LAST_MESSAGE 36 | "recent-rooms.accessibility-label.dm" = "與%@私訊,%@ %@"; 37 | "recent-rooms.new-message-placeholder" = "新訊息"; 38 | "recent-rooms.accessibility-label.new-conversation" = "新對話"; 39 | "recent-rooms.accessibility-label.settings" = "設定"; 40 | "loading.cancel" = "取消"; 41 | "loading.4" = "回復作業階段"; 42 | "loading.3" = "登入中"; 43 | "login.failure-back-to-login" = "返回登入頁"; 44 | "login.register-not-yet-implemented" = "尚未提供創建新帳戶功能。"; 45 | "login.open-registration-prompt" = "還沒有帳戶?"; 46 | "login.sign-in" = "登入"; 47 | "login.form.homeserver-optional-explanation" = "若您選用matrix.org則不必填寫家伺服器。"; 48 | "login.form.homeserver" = "家伺服器"; 49 | "login.form.password" = "密碼"; 50 | "login.form.username" = "使用者名稱"; 51 | "login.welcome-message" = "從登入帳戶開始。"; 52 | "login.welcome-header" = "歡迎使用 "; 53 | "event.context-menu.remove" = "移除"; 54 | "event.context-menu.edit" = "編輯"; 55 | "event.context-menu.reply" = "回覆"; 56 | "event.context-menu.add-reaction" = "加入反應"; 57 | "event.edited" = "已編輯"; 58 | "event.redaction.redact-other" = "%@ 移除了 %@ 的訊息"; 59 | "event.redaction.redact-self" = "訊息已被 %@ 移除"; 60 | "event.room-power-level.change-other" = "%@ 將權限等級從 %@ 更改為 %@"; 61 | "event.room-power-level.change-self" = "%@ 將權限等級從 %@ 更改為 %@"; 62 | "room-power-level.custom" = "自訂權限(%d)"; 63 | "room-power-level.admin" = "管理員"; 64 | "room-power-level.moderator" = "版主"; 65 | "room-power-level.default" = "預設"; 66 | "event.room-topic.change" = "%@ 將主題更改為「%@」"; 67 | "event.room-name.change-name" = "%@ 將聊天室名稱從 %@ 更改為 %@"; 68 | "event.room-name.set-name" = "%@ 將聊天室名稱設定為 %@"; 69 | "event.reason" = "原由:%@"; 70 | "event.room-member.unknown-state" = "未知的狀態事件:%@"; 71 | "event.room-member.remove-avatar" = "%@ 移除了大頭照"; 72 | "event.room-member.change-avatar" = "%@ 更新了大頭照"; 73 | "event.room-member.set-avatar" = "%@ 設定了大頭照"; 74 | "event.room-member.change-name" = "%@ 將顯示名稱更改為 %@"; 75 | "new-conversation.done" = "完成"; 76 | "new-conversation.edit" = "編輯"; 77 | "new-conversation.cancel" = "取消"; 78 | "new-conversation.alert-failed" = "開始聊天發生錯誤"; 79 | "new-conversation.create-room" = "開始聊天"; 80 | "new-conversation.public-room" = "公開聊天室"; 81 | "new-conversation.room-name" = "聊天室名稱"; 82 | "new-conversation.for-example" = "例如"; 83 | "new-conversation.username-placeholder" = "Matrix ID"; 84 | "new-conversation.title-room" = "新聊天室"; 85 | "new-conversation.title-chat" = "新聊天對話"; 86 | "settings.dismiss" = "完成"; 87 | "settings.log-out" = "登出"; 88 | "settings.app-icon" = "應用程式圖示"; 89 | "settings.accent-color" = "強調色"; 90 | "settings.title" = "設定"; 91 | "reaction-picker.title" = "點擊顏文字以傳送您的回應。"; 92 | "loading.2" = "打亂訊息"; 93 | "loading.1" = "讓線條變成網狀"; 94 | -------------------------------------------------------------------------------- /Nio/Utility/Formatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum DateFormat: String { 4 | case yearMonthDay = "yyyy-MM-dd" 5 | } 6 | 7 | enum Formatter { 8 | // MARK: Relative Date Formatter 9 | static private func relativeDateTimeFormatter(dateTimeStyle: RelativeDateTimeFormatter.DateTimeStyle? = nil, 10 | unitsStyle: RelativeDateTimeFormatter.UnitsStyle? = nil, 11 | formattingContext: RelativeDateTimeFormatter.Context?, 12 | locale: Locale) -> RelativeDateTimeFormatter { 13 | let formatter = RelativeDateTimeFormatter() 14 | if let dateTimeStyle = dateTimeStyle { 15 | formatter.dateTimeStyle = dateTimeStyle 16 | } 17 | if let unitsStyle = unitsStyle { 18 | formatter.unitsStyle = unitsStyle 19 | } 20 | if let formattingContext = formattingContext { 21 | formatter.formattingContext = formattingContext 22 | } 23 | formatter.locale = locale 24 | return formatter 25 | } 26 | 27 | static func string(forRelativeDate relativeDate: Date, 28 | to otherDate: Date = Date(), 29 | context: RelativeDateTimeFormatter.Context? = nil, 30 | locale: Locale = .current) -> String { 31 | relativeDateTimeFormatter(dateTimeStyle: .named, unitsStyle: .full, formattingContext: context, locale: locale) 32 | .localizedString(for: relativeDate, relativeTo: otherDate) 33 | } 34 | 35 | // MARK: Date Formatter 36 | private static func dateFormatter(format: DateFormat? = nil, 37 | dateStyle: DateFormatter.Style? = nil, 38 | timeStyle: DateFormatter.Style? = nil, 39 | locale: Locale) -> DateFormatter { 40 | let formatter = DateFormatter() 41 | if let format = format { 42 | formatter.dateFormat = format.rawValue 43 | } 44 | if let dateStyle = dateStyle { 45 | formatter.dateStyle = dateStyle 46 | } 47 | if let timeStyle = timeStyle { 48 | formatter.timeStyle = timeStyle 49 | } 50 | formatter.locale = locale 51 | return formatter 52 | } 53 | 54 | static func string(for date: Date, format: DateFormat, locale: Locale = .current) -> String { 55 | let formatter = dateFormatter(format: format, locale: locale) 56 | return formatter.string(from: date) 57 | } 58 | 59 | static func string(for date: Date, dateStyle: DateFormatter.Style, locale: Locale = .current) -> String { 60 | string(for: date, dateStyle: dateStyle, timeStyle: .none, locale: locale) 61 | } 62 | 63 | static func string(for date: Date, timeStyle: DateFormatter.Style, locale: Locale = .current) -> String { 64 | string(for: date, dateStyle: .none, timeStyle: timeStyle, locale: locale) 65 | } 66 | 67 | static func string(for date: Date, 68 | dateStyle: DateFormatter.Style, 69 | timeStyle: DateFormatter.Style, 70 | locale: Locale = .current) -> String { 71 | let formatter = dateFormatter(dateStyle: dateStyle, timeStyle: timeStyle, locale: locale) 72 | return formatter.string(from: date) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Nio/Utility/GroupedEvents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftMatrixSDK 3 | 4 | struct GroupBounds: OptionSet, Equatable, Hashable { 5 | let rawValue: Int 6 | 7 | static let before: Self = .init(rawValue: 1 << 0) 8 | static let after: Self = .init(rawValue: 1 << 1) 9 | 10 | static let both: Self = [.before, .after] 11 | } 12 | 13 | struct Bounded { 14 | var wrapped: Wrapped 15 | var bounds: GroupBounds 16 | 17 | init(_ wrapped: Wrapped, bounds: GroupBounds) { 18 | self.wrapped = wrapped 19 | self.bounds = bounds 20 | } 21 | } 22 | 23 | extension Bounded: Equatable where Wrapped: Equatable {} 24 | 25 | extension Bounded: Hashable where Wrapped: Hashable {} 26 | 27 | extension Bounded: Identifiable where Wrapped: Identifiable { 28 | // swiftlint:disable:next type_name 29 | typealias ID = Wrapped.ID 30 | 31 | var id: ID { 32 | wrapped.id 33 | } 34 | } 35 | 36 | extension Bounded where Wrapped == MXEvent { 37 | var event: MXEvent { 38 | get { wrapped } 39 | set { wrapped = newValue } 40 | } 41 | } 42 | 43 | struct EventGroup { 44 | let events: [MXEvent] 45 | 46 | init(_ events: [MXEvent] = []) { 47 | self.events = events 48 | } 49 | } 50 | 51 | struct EventGroups { 52 | var groups: [EventGroup] 53 | 54 | init(_ events: [MXEvent]) { 55 | self.groups = Self.groups(from: events) 56 | } 57 | 58 | // Collects events in groups of equal `.sender`: 59 | static func groups(from events: S) -> [EventGroup] where S: Sequence, S.Element == MXEvent { 60 | let iterator = events.makeIterator() 61 | let groupingIterator = GroupingIterator(iterator) { $0.sender } 62 | return IteratorSequence(groupingIterator).map { EventGroup($0) } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Nio/Utility/GroupingIterator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An iterator with a grouping closure that returns sub-sequences next element 4 | /// without removing it without advancing the iterator. 5 | struct GroupingIterator: IteratorProtocol where Iterator: IteratorProtocol { 6 | typealias Element = [Iterator.Element] 7 | typealias Closure = (Iterator.Element, Iterator.Element) -> Bool 8 | 9 | private var iterator: PeekableIterator 10 | private let closure: Closure 11 | 12 | /// Creates an grouping iterator, wrapping `iterator`. 13 | /// - Parameters: 14 | /// - iterator: the wrapped iterator 15 | /// - closure: A predicate that returns true if its second argument 16 | /// should be added to the same group as the first argument; 17 | /// otherwise, false. 18 | init(_ iterator: Iterator, by closure: @escaping Closure) { 19 | self.iterator = .init(iterator) 20 | self.closure = closure 21 | } 22 | 23 | mutating func next() -> Element? { 24 | guard var previous = iterator.next() else { 25 | return nil 26 | } 27 | 28 | var group: [Iterator.Element] = [previous] 29 | 30 | while let peek = iterator.peek() { 31 | guard closure(previous, peek) else { 32 | break 33 | } 34 | 35 | // The forced unwrap is safe here, due to having peeked above: 36 | let element = iterator.next()! 37 | 38 | group.append(element) 39 | 40 | previous = element 41 | } 42 | 43 | return group 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Nio/Utility/MXURL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | struct MXURL { 5 | var mxContentURI: URL 6 | 7 | init?(mxContentURI: String) { 8 | guard let uri = URL(string: mxContentURI) else { 9 | return nil 10 | } 11 | self.mxContentURI = uri 12 | } 13 | 14 | func contentURL(on homeserver: URL) -> URL? { 15 | var components = URLComponents() 16 | components.scheme = "https" 17 | components.host = homeserver.host 18 | guard let contentHost = mxContentURI.host else { return nil } 19 | components.path = "/_matrix/media/r0/download/\(contentHost)/\(mxContentURI.lastPathComponent)" 20 | return components.url 21 | } 22 | } 23 | 24 | extension MXURL { 25 | static var nioIconURL: URL { 26 | Self.nioIcon.contentURL(on: URL(string: "https://matrix.org")!)! 27 | } 28 | 29 | static var nioIcon: MXURL { 30 | MXURL(mxContentURI: "mxc://matrix.org/rdElwkPTTrdZljUuKwkSEMqV")! 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Nio/Utility/PeekableIterator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An iterator with a `peek()` that returns the next element 4 | /// without removing it without advancing the iterator. 5 | struct PeekableIterator: IteratorProtocol where Iterator: IteratorProtocol { 6 | typealias Element = Iterator.Element 7 | 8 | private var iterator: Iterator 9 | private var peeked: Element? 10 | 11 | /// Creates a peekable iterator, wrapping `iterator`. 12 | /// - Parameter iterator: the wrapped iterator 13 | init(_ iterator: Iterator) { 14 | self.iterator = iterator 15 | } 16 | 17 | /// Advances the iterator and returns the next value. 18 | mutating func next() -> Element? { 19 | guard let peeked = peeked else { 20 | return iterator.next() 21 | } 22 | defer { self.peeked = nil } 23 | return peeked 24 | } 25 | 26 | /// Returns the `next()` value without advancing the iterator. 27 | mutating func peek() -> Element? { 28 | if peeked == nil { 29 | peeked = iterator.next() 30 | } 31 | return peeked 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Nio/Utility/TypedEvents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | protocol TypeableEvent { 5 | var type: String { get } 6 | var sender: String { get } 7 | } 8 | 9 | /// An event and its type 10 | struct TypedEvent where Event: TypeableEvent { 11 | let type: MXEventType 12 | let event: Event 13 | } 14 | 15 | extension TypedEvent: Equatable where Event: Equatable { 16 | static func == (lhs: TypedEvent, rhs: TypedEvent) -> Bool { 17 | guard lhs.type == rhs.type else { 18 | return false 19 | } 20 | 21 | guard lhs.event == rhs.event else { 22 | return false 23 | } 24 | 25 | return true 26 | } 27 | } 28 | 29 | /// A non-empty group of homogeneous events 30 | struct TypedEventGroup where Event: TypeableEvent { 31 | let type: MXEventType 32 | let events: [Event] 33 | 34 | init(type: MXEventType, events: [TypedEvent]) { 35 | let events = events.map { $0.event } 36 | self.init(type: type, events: events) 37 | } 38 | 39 | init(type: MXEventType, events: [Event]) { 40 | assert(!events.isEmpty) 41 | assert(events.allSatisfy { event in 42 | MXEventType(identifier: event.type) == type 43 | }) 44 | 45 | self.type = type 46 | self.events = events 47 | } 48 | } 49 | 50 | extension TypedEventGroup: Equatable where Event: Equatable { 51 | static func == (lhs: TypedEventGroup, rhs: TypedEventGroup) -> Bool { 52 | lhs.events == rhs.events 53 | } 54 | } 55 | 56 | /// A collection of groups of events of equal type. 57 | struct TypedEventGroups: Collection where Event: TypeableEvent { 58 | typealias Index = Int 59 | 60 | var startIndex: Int { 61 | 0 62 | } 63 | 64 | var endIndex: Int { 65 | groups.count 66 | } 67 | 68 | let groups: [TypedEventGroup] 69 | 70 | init(events: S) where S: Sequence, S.Element == Event { 71 | self.init(groups: Self.grouped(events: events)) 72 | } 73 | 74 | init(groups: [TypedEventGroup]) { 75 | self.groups = groups 76 | } 77 | 78 | private static func grouped(events: S) -> [TypedEventGroup] where S: Sequence, S.Element == Event { 79 | let typedEvents: LazyMapSequence> = events.lazy.map { event in 80 | let type = MXEventType(identifier: event.type) 81 | return TypedEvent(type: type, event: event) 82 | } 83 | let iterator = typedEvents.makeIterator() 84 | let groupingIterator = GroupingIterator(iterator) { lhs, rhs in 85 | // Split the events into groups of consecutive events of same type: 86 | guard lhs.type == rhs.type else { 87 | return false 88 | } 89 | switch lhs.type { 90 | // Perform additional specialized grouping for message events: 91 | case .roomMessage, .roomMessageFeedback: 92 | // Groups into consecutive events of same sender: 93 | return lhs.event.sender == rhs.event.sender 94 | case _: 95 | return true 96 | } 97 | } 98 | let groups = IteratorSequence(groupingIterator) 99 | return groups.compactMap { events in 100 | guard let event = events.first else { 101 | return nil 102 | } 103 | return TypedEventGroup(type: event.type, events: events) 104 | } 105 | } 106 | 107 | subscript(position: Int) -> TypedEventGroup { 108 | groups[position] 109 | } 110 | 111 | func index(after index: Int) -> Int { 112 | index + 1 113 | } 114 | } 115 | 116 | extension TypedEventGroups: Equatable where Event: Equatable { 117 | static func == (lhs: TypedEventGroups, rhs: TypedEventGroups) -> Bool { 118 | lhs.groups == rhs.groups 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /NioKit/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | class Configuration { 5 | static func setupMatrixSDKSettings() { 6 | let sdkOptions = MXSDKOptions.sharedInstance() 7 | 8 | // sdkOptions.applicationGroupIdentifier = "" 9 | 10 | // Enable e2e encryption for newly created MXSession 11 | sdkOptions.enableCryptoWhenStartingMXSession = true 12 | sdkOptions.computeE2ERoomSummaryTrust = true 13 | 14 | // Use UIKit BackgroundTask for handling background tasks in the SDK 15 | // sdkOptions.backgroundModeHandler = MXUIKitBackgroundModeHandler() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NioKit/Extensions/MX+Identifiable.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MatrixSDK 3 | 4 | extension MXPublicRoom: Identifiable {} 5 | extension MXRoom: Identifiable {} 6 | extension MXEvent: Identifiable {} 7 | -------------------------------------------------------------------------------- /NioKit/Extensions/MXClient+Publisher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import MatrixSDK 4 | 5 | extension MXRestClient { 6 | public func nio_publicRooms(onServer: String? = nil, limit: UInt? = nil) -> AnyPublisher { 7 | Future { promise in 8 | self.publicRooms(onServer: onServer, limit: limit) { response in 9 | switch response { 10 | case .failure(let error): 11 | promise(.failure(error)) 12 | case .success(let publicRoomsResponse): 13 | promise(.success(publicRoomsResponse)) 14 | @unknown default: 15 | fatalError("Unexpected Matrix response: \(response)") 16 | } 17 | } 18 | }.eraseToAnyPublisher() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NioKit/Extensions/MXCredentials+Keychain.swift: -------------------------------------------------------------------------------- 1 | import MatrixSDK 2 | import KeychainAccess 3 | 4 | extension MXCredentials { 5 | public func save(to keychain: Keychain) { 6 | guard 7 | let homeserver = self.homeServer, 8 | let userId = self.userId, 9 | let accessToken = self.accessToken, 10 | let deviceId = self.deviceId 11 | else { 12 | return 13 | } 14 | keychain["homeserver"] = homeserver 15 | keychain["userId"] = userId 16 | keychain["accessToken"] = accessToken 17 | keychain["deviceId"] = deviceId 18 | } 19 | 20 | public func clear(from keychain: Keychain) { 21 | keychain["homeserver"] = nil 22 | keychain["userId"] = nil 23 | keychain["accessToken"] = nil 24 | keychain["deviceId"] = nil 25 | } 26 | 27 | public static func from(_ keychain: Keychain) -> MXCredentials? { 28 | guard 29 | let homeserver = keychain["homeserver"], 30 | let userId = keychain["userId"], 31 | let accessToken = keychain["accessToken"], 32 | let deviceId = keychain["deviceId"] 33 | else { 34 | return nil 35 | } 36 | let credentials = MXCredentials( 37 | homeServer: homeserver, 38 | userId: userId, 39 | accessToken: accessToken 40 | ) 41 | credentials.deviceId = deviceId 42 | return credentials 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NioKit/Extensions/MXEvent+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | extension MXEvent { 5 | public var timestamp: Date { 6 | Date(timeIntervalSince1970: TimeInterval(self.originServerTs / 1000)) 7 | } 8 | 9 | public func content(valueFor key: String) -> T? { 10 | if let value = self.content?[key] as? T { 11 | return value 12 | } 13 | return nil 14 | } 15 | 16 | public func prevContent(valueFor key: String) -> T? { 17 | if let value = self.unsignedData?.prevContent?[key] as? T { 18 | return value 19 | } 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NioKit/Extensions/UXKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UXKit.swift 3 | // Nio 4 | // 5 | // Created by Helge Heß on 05.03.21. 6 | // Copyright © 2021 Kilian Koeltzsch. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | 12 | public typealias UXColor = NSColor 13 | public typealias UXImage = NSImage 14 | public typealias UXEdgeInsets = NSEdgeInsets 15 | public typealias UXFont = NSFont 16 | 17 | public enum UXFakeTraitCollection { 18 | case current 19 | } 20 | 21 | public extension NSColor { 22 | 23 | @inlinable 24 | func resolvedColor(with fakeTraitCollection: UXFakeTraitCollection) 25 | -> UXColor 26 | { 27 | return self 28 | } 29 | } 30 | 31 | #if canImport(SwiftUI) 32 | import SwiftUI 33 | 34 | public enum UXFakeDisplayMode { 35 | case inline, automatic, large 36 | } 37 | public enum UXFakeAutocapitalizationMode { 38 | case none 39 | } 40 | 41 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 42 | public extension View { 43 | 44 | @inlinable 45 | func navigationBarTitle( 46 | _ title: S, displayMode: UXFakeDisplayMode = .inline 47 | ) -> some View 48 | { 49 | self.navigationTitle(title) 50 | } 51 | 52 | @inlinable 53 | func autocapitalization(_ mode: UXFakeAutocapitalizationMode) -> Self { 54 | return self 55 | } 56 | } 57 | #endif 58 | #elseif canImport(UIKit) 59 | import UIKit 60 | 61 | public typealias UXColor = UIColor 62 | public typealias UXImage = UIImage 63 | public typealias UXEdgeInsets = UIEdgeInsets 64 | public typealias UXFont = UIFont 65 | 66 | #if canImport(SwiftUI) 67 | import SwiftUI 68 | 69 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 70 | public extension View { 71 | } 72 | #endif 73 | #else 74 | #error("GNUstep not yet supported, sorry!") 75 | #endif 76 | -------------------------------------------------------------------------------- /NioKit/Extensions/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults.swift 3 | // NioKit 4 | // 5 | // Created by Stefan Hofman on 05/09/2020. 6 | // Copyright © 2020 Kilian Koeltzsch. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension UserDefaults { 12 | private static let appGroup: String = { 13 | guard let group = Bundle.main.infoDictionary?["AppGroup"] as? String else { 14 | fatalError("Missing 'AppGroup' key in Info.plist!") 15 | } 16 | return group 17 | }() 18 | #if os(macOS) 19 | private static let teamIdentifierPrefix = Bundle.main 20 | .object(forInfoDictionaryKey: "TeamIdentifierPrefix") as? String ?? "" 21 | 22 | private static let suiteName = teamIdentifierPrefix + appGroup 23 | #else // iOS 24 | private static let suiteName = "group." + appGroup 25 | #endif 26 | 27 | static let group = UserDefaults(suiteName: suiteName)! 28 | } 29 | -------------------------------------------------------------------------------- /NioKit/Models/Custom Events/CustomEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol CustomEvent { 4 | func encodeContent() throws -> [String: Any] 5 | } 6 | -------------------------------------------------------------------------------- /NioKit/Models/Custom Events/EditEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | struct EditEvent { 5 | let eventId: String 6 | let text: String 7 | } 8 | 9 | extension EditEvent: CustomEvent { 10 | func encodeContent() throws -> [String: Any] { 11 | [ 12 | "body": "*" + text, 13 | "m.new_content": [ 14 | "body": text, 15 | "msgtype": kMXMessageTypeText 16 | ], 17 | "m.relates_to": [ 18 | "event_id": eventId, 19 | "rel_type": MXEventRelationTypeReplace 20 | ], 21 | "msgtype": kMXMessageTypeText 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NioKit/Models/Custom Events/ReactionEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | struct ReactionEvent { 5 | let eventId: String 6 | let key: String 7 | let relType = MXEventRelationTypeAnnotation 8 | } 9 | 10 | extension ReactionEvent: CustomEvent { 11 | func encodeContent() throws -> [String: Any] { 12 | [ 13 | "m.relates_to": [ 14 | "event_id": eventId, 15 | "key": key, 16 | "rel_type": relType 17 | ] 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NioKit/Models/NIORoomSummary.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MatrixSDK 3 | 4 | @dynamicMemberLookup 5 | public class NIORoomSummary: ObservableObject { 6 | internal var summary: MXRoomSummary 7 | 8 | public var lastMessageDate: Date { 9 | let timestamp = Double(summary.lastMessage.originServerTs) 10 | return Date(timeIntervalSince1970: timestamp / 1000) 11 | } 12 | 13 | public init(_ summary: MXRoomSummary) { 14 | self.summary = summary 15 | } 16 | 17 | public subscript(dynamicMember keyPath: KeyPath) -> T { 18 | summary[keyPath: keyPath] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NioKit/Models/Reaction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Reaction: Identifiable { 4 | public var id: String 5 | public let sender: String 6 | public let timestamp: Date 7 | public let reaction: String 8 | 9 | public init( 10 | id: String, 11 | sender: String, 12 | timestamp: Date, 13 | reaction: String 14 | ) { 15 | self.id = id 16 | self.sender = sender 17 | self.timestamp = timestamp 18 | self.reaction = reaction 19 | } 20 | } 21 | 22 | public struct ReactionGroup: Identifiable { 23 | public let reaction: String 24 | public let count: Int 25 | public let reactions: [Reaction] 26 | 27 | public var id: String { 28 | self.reaction 29 | } 30 | 31 | public init(reaction: String, count: Int, reactions: [Reaction]) { 32 | self.reaction = reaction 33 | self.count = count 34 | self.reactions = reactions 35 | } 36 | 37 | public func containsReaction(from sender: String) -> Bool { 38 | self.reactions.contains { $0.sender == sender } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NioKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NioKitTests/NioKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import NioKit 4 | 5 | class NioKitTests: XCTestCase { 6 | override func setUpWithError() throws { 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | } 9 | 10 | override func tearDownWithError() throws { 11 | // Put teardown code here. This method is called after the invocation of each test method in the class. 12 | } 13 | 14 | func testExample() throws { 15 | // This is an example of a functional test case. 16 | // Use XCTAssert and related functions to verify your tests produce the correct results. 17 | } 18 | 19 | func testPerformanceExample() throws { 20 | // This is an example of a performance test case. 21 | self.measure { 22 | // Put the code you want to measure the time of here. 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NioShareExtension/GetURL.js: -------------------------------------------------------------------------------- 1 | var GetURL = function() {}; 2 | GetURL.prototype = { 3 | run: function(arguments) { 4 | arguments.completionFunction({"url": document.URL}); 5 | } 6 | }; 7 | var ExtensionPreprocessingJS = new GetURL; 8 | -------------------------------------------------------------------------------- /NioShareExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppGroup 6 | $(APPGROUP) 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | NioShareExtension 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | DevelopmentTeam 26 | $(DEVELOPMENT_TEAM) 27 | NSExtension 28 | 29 | NSExtensionAttributes 30 | 31 | IntentsSupported 32 | 33 | NSExtensionActivationRule 34 | 35 | NSExtensionActivationSupportsWebURLWithMaxCount 36 | 1 37 | 38 | NSExtensionJavaScriptPreprocessingFile 39 | GetURL 40 | 41 | NSExtensionPointIdentifier 42 | com.apple.share-services 43 | NSExtensionPrincipalClass 44 | ShareNavigationController 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /NioShareExtension/NioShareExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.$(APPGROUP) 10 | 11 | com.apple.security.network.client 12 | 13 | keychain-access-groups 14 | 15 | $(AppIdentifierPrefix)nio.keychain 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /NioShareExtension/ShareContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import NioKit 3 | import UIKit 4 | 5 | struct ShareContentView: View { 6 | @State var parentView: ShareNavigationController 7 | @State var showConfirm = false 8 | @State var selectedRoom: RoomItem? 9 | 10 | var rooms: [RoomItem]? 11 | 12 | public init(parentView: ShareNavigationController) { 13 | _parentView = State(initialValue: parentView) 14 | let data = UserDefaults.group.data(forKey: "roomList")! 15 | do { 16 | let decoder = JSONDecoder() 17 | let temp = try decoder.decode([RoomItem].self, from: data) 18 | self.rooms = temp 19 | self.rooms?.sort(by: { $0.messageDate > $1.messageDate }) 20 | } catch { 21 | print("An error occured: \(error)") 22 | } 23 | } 24 | 25 | var body: some View { 26 | NavigationView { 27 | VStack { 28 | List(rooms!, id: \.self) { room in 29 | Button(action: { 30 | self.selectedRoom = room 31 | self.showConfirm.toggle() 32 | }, label: { 33 | Text(room.displayName) 34 | }) 35 | } 36 | .padding(.bottom, 20.0) 37 | .listStyle(GroupedListStyle()) 38 | .navigationBarTitle("Nio") 39 | .alert(isPresented: $showConfirm) { 40 | Alert( 41 | title: Text("Send to " + self.selectedRoom!.displayName), 42 | primaryButton: .default( 43 | Text("Send"), 44 | action: { 45 | self.parentView.didSelectPost(roomID: self.selectedRoom!.roomId) 46 | }), 47 | secondaryButton: .cancel()) 48 | } 49 | // This is ugly, but otherwise the last results are too far down. Update if you know 50 | // a better way. 51 | Text("") 52 | Text("") 53 | Text("") 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct ShareContentView_Previews: PreviewProvider { 60 | static var previews: some View { 61 | /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NioShareExtension/ShareViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Social 3 | import MobileCoreServices 4 | import SwiftUI 5 | import NioKit 6 | import MatrixSDK 7 | 8 | @objc(ShareNavigationController) 9 | class ShareNavigationController: UIViewController { 10 | 11 | let store: AccountStore = AccountStore.init() 12 | 13 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 14 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 15 | self.modalPresentationStyle = UIModalPresentationStyle.fullScreen 16 | let childView = UIHostingController(rootView: ShareContentView(parentView: self)) 17 | addChild(childView) 18 | childView.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height) 19 | view.addSubview(childView.view) 20 | childView.didMove(toParent: self) 21 | } 22 | 23 | func didSelectPost(roomID: String) { 24 | switch store.loginState { 25 | case .loggedIn(_): 26 | let propertyList = String(kUTTypePropertyList) 27 | let rooms = store.rooms 28 | if let item = extensionContext?.inputItems.first as? NSExtensionItem, 29 | let itemProvider = item.attachments?.first, 30 | itemProvider.hasItemConformingToTypeIdentifier(propertyList) { 31 | itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil) { (item, _) in 32 | guard let dictionary = item as? NSDictionary, 33 | let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary, 34 | let url = results["url"] as? String else { 35 | return 36 | } 37 | for room in rooms where room.summary.roomId == roomID { 38 | room.send(text: url) 39 | } 40 | self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) 41 | } 42 | } 43 | case .loggedOut: 44 | didSelectCancel() 45 | case .authenticating: 46 | let alert = UIAlertController(title: nil, message: "Sending...", preferredStyle: .alert) 47 | 48 | let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50)) 49 | loadingIndicator.hidesWhenStopped = true 50 | loadingIndicator.style = UIActivityIndicatorView.Style.medium 51 | loadingIndicator.startAnimating() 52 | 53 | alert.view.addSubview(loadingIndicator) 54 | present(alert, animated: true, completion: nil) 55 | case .failure(_): 56 | didSelectCancel() 57 | } 58 | } 59 | 60 | enum ShareViewControllerError: Error { 61 | case assertionError(description: String) 62 | case unsupportedMedia 63 | case notRegistered 64 | case obsoleteShare 65 | } 66 | 67 | func didSelectCancel() { 68 | self.dismiss(animated: true) { 69 | self.extensionContext!.cancelRequest(withError: ShareViewControllerError.obsoleteShare) 70 | } 71 | } 72 | 73 | @available(*, unavailable) 74 | required init?(coder aDecoder: NSCoder) { 75 | super.init(coder: aDecoder) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /NioTests/EventCollectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MatrixSDK 3 | 4 | @testable import Nio 5 | @testable import NioKit 6 | 7 | // swiftlint:disable identifier_name 8 | 9 | class EventCollectionTests: XCTestCase { 10 | func testReadConnectedEdges() { 11 | let m = kMXEventTypeStringRoomMessage 12 | 13 | let m1 = MockEvent(sender: "A", type: m, timestamp: 0, isRedacted: false) 14 | let m2 = MockEvent(sender: "A", type: m, timestamp: 0, isRedacted: false) 15 | let m3 = MockEvent(sender: "A", type: m, timestamp: 0, isRedacted: false) 16 | 17 | let events = [m1, m2, m3] 18 | 19 | XCTAssertEqual(EventCollection(events).connectedEdges(of: m1), [.bottomEdge]) 20 | XCTAssertEqual(EventCollection(events).connectedEdges(of: m2), [.topEdge, .bottomEdge]) 21 | XCTAssertEqual(EventCollection(events).connectedEdges(of: m3), [.topEdge]) 22 | 23 | // FIXME: This obviously needs to cover all cases. 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NioTests/GroupingIteratorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Nio 4 | 5 | class GroupingIteratorTests: XCTestCase { 6 | func testGroupingIteratorShouldSplitElementsByClosure() { 7 | let elements: [Int] = [0, 1, 3, 2, 3] 8 | let iterator = elements.makeIterator() 9 | let groupingIterator: GroupingIterator = .init(iterator) { 10 | // Group elements by their even/odd-ness: 11 | ($0 % 2) == ($1 % 2) 12 | } 13 | 14 | let expected: [[Int]] = [ 15 | [0], 16 | [1, 3], 17 | [2], 18 | [3] 19 | ] 20 | let actual = Array(IteratorSequence(groupingIterator)) 21 | XCTAssertEqual(actual, expected) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NioTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NioTests/Mocks/MockEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import class MatrixSDK.MXEvent 3 | 4 | class MockEvent: MXEvent { 5 | init(sender: String, type: String, timestamp: UInt64, isRedacted: Bool) { 6 | self._type = type 7 | self.isRedacted = isRedacted 8 | super.init() 9 | self.sender = sender 10 | self.originServerTs = 1000 * timestamp 11 | } 12 | 13 | // swiftlint:disable:next identifier_name 14 | var _type: String 15 | override var type: String! { 16 | _type 17 | } 18 | 19 | var isRedacted: Bool 20 | override func isRedactedEvent() -> Bool { 21 | isRedacted 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NioTests/PeekableIteratorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Nio 4 | 5 | class PeekableIteratorTests: XCTestCase { 6 | func testNextShouldRemoveAndReturnNextElement() { 7 | let elements = [0, 1, 2, 3, 4] 8 | let iterator = elements.makeIterator() 9 | var peekableIterator = PeekableIterator(iterator) 10 | 11 | let actual0 = peekableIterator.next() 12 | let expected0 = 0 13 | XCTAssertEqual(actual0, expected0) 14 | 15 | let actual1 = peekableIterator.next() 16 | let expected1 = 1 17 | XCTAssertEqual(actual1, expected1) 18 | } 19 | 20 | func testPeekShouldReturnButNotRemoveNextElement() { 21 | let elements = [0, 1, 2, 3, 4] 22 | let iterator = elements.makeIterator() 23 | var peekableIterator = PeekableIterator(iterator) 24 | 25 | // Calling peek should return the next element: 26 | let actual0 = peekableIterator.peek() 27 | let expected0 = 0 28 | XCTAssertEqual(actual0, expected0) 29 | 30 | // Calling peek again should return the same element: 31 | let actual1 = peekableIterator.peek() 32 | let expected1 = 0 33 | XCTAssertEqual(actual1, expected1) 34 | } 35 | 36 | func testNextAfterPeekShouldRemoveAndReturnPeekedElement() { 37 | let elements = [0, 1, 2, 3, 4] 38 | let iterator = elements.makeIterator() 39 | var peekableIterator = PeekableIterator(iterator) 40 | 41 | // Calling peek should return the next element: 42 | let actual0 = peekableIterator.peek() 43 | let expected0 = 0 44 | XCTAssertEqual(actual0, expected0) 45 | 46 | // Calling next should return the peeked element: 47 | let actual1 = peekableIterator.next() 48 | let expected1 = 0 49 | XCTAssertEqual(actual1, expected1) 50 | } 51 | 52 | func testPeekAfterNextShouldRemoveAndReturnPeekedElement() { 53 | let elements = [0, 1, 2, 3, 4] 54 | let iterator = elements.makeIterator() 55 | var peekableIterator = PeekableIterator(iterator) 56 | 57 | // Calling next should return the next element: 58 | let actual0 = peekableIterator.next() 59 | let expected0 = 0 60 | XCTAssertEqual(actual0, expected0) 61 | 62 | // Calling peek should return the next element: 63 | let actual1 = peekableIterator.peek() 64 | let expected1 = 1 65 | XCTAssertEqual(actual1, expected1) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /NioTests/TypedEventsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MatrixSDK 3 | 4 | @testable import Nio 5 | 6 | class TypedEventsTests: XCTestCase { 7 | struct Event: TypeableEvent, Equatable { 8 | let id: Int 9 | let type: String 10 | let sender: String 11 | 12 | public init(id: Int, type: MXEventType, sender: String) { 13 | self.id = id 14 | self.type = type.identifier 15 | self.sender = sender 16 | } 17 | } 18 | 19 | func testGroupHomogeneousMessages() { 20 | let events: [Event] = [ 21 | Event(id: 0, type: .roomMessage, sender: "Alice"), 22 | Event(id: 1, type: .roomMessage, sender: "Alice"), 23 | Event(id: 2, type: .roomMessage, sender: "Bob"), 24 | Event(id: 3, type: .roomMessage, sender: "Eve"), 25 | Event(id: 4, type: .roomMessage, sender: "Eve"), 26 | Event(id: 5, type: .roomMessage, sender: "Bob") 27 | ] 28 | 29 | let actual = TypedEventGroups(events: events) 30 | let expected: TypedEventGroups = TypedEventGroups(groups: [ 31 | TypedEventGroup(type: .roomMessage, events: [ 32 | Event(id: 0, type: .roomMessage, sender: "Alice"), 33 | Event(id: 1, type: .roomMessage, sender: "Alice") 34 | ]), 35 | TypedEventGroup(type: .roomMessage, events: [ 36 | Event(id: 2, type: .roomMessage, sender: "Bob") 37 | ]), 38 | TypedEventGroup(type: .roomMessage, events: [ 39 | Event(id: 3, type: .roomMessage, sender: "Eve"), 40 | Event(id: 4, type: .roomMessage, sender: "Eve") 41 | ]), 42 | TypedEventGroup(type: .roomMessage, events: [ 43 | Event(id: 5, type: .roomMessage, sender: "Bob") 44 | ]) 45 | ]) 46 | 47 | XCTAssertEqual(actual, expected) 48 | } 49 | 50 | func testGroupHeterogeneousEvents() { 51 | let events: [Event] = [ 52 | Event(id: 0, type: .roomMessage, sender: "Alice"), 53 | Event(id: 1, type: .roomMessage, sender: "Alice"), 54 | Event(id: 2, type: .presence, sender: "Alice"), 55 | Event(id: 3, type: .roomMessage, sender: "Eve"), 56 | Event(id: 4, type: .roomMessage, sender: "Eve"), 57 | Event(id: 5, type: .roomMessage, sender: "Bob") 58 | ] 59 | 60 | let actual = TypedEventGroups(events: events) 61 | let expected: TypedEventGroups = TypedEventGroups(groups: [ 62 | TypedEventGroup(type: .roomMessage, events: [ 63 | Event(id: 0, type: .roomMessage, sender: "Alice"), 64 | Event(id: 1, type: .roomMessage, sender: "Alice") 65 | ]), 66 | TypedEventGroup(type: .presence, events: [ 67 | Event(id: 2, type: .presence, sender: "Alice") 68 | ]), 69 | TypedEventGroup(type: .roomMessage, events: [ 70 | Event(id: 3, type: .roomMessage, sender: "Eve"), 71 | Event(id: 4, type: .roomMessage, sender: "Eve") 72 | ]), 73 | TypedEventGroup(type: .roomMessage, events: [ 74 | Event(id: 5, type: .roomMessage, sender: "Bob") 75 | ]) 76 | ]) 77 | 78 | XCTAssertEqual(actual, expected) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

💬 Nio 2 | 4 |

5 | 6 | [![Matrix](https://img.shields.io/matrix/niochat:matrix.org?logo=matrix&style=flat)](https://matrix.to/#/#niochat:matrix.org) 7 | [![Liberapay](https://img.shields.io/liberapay/receives/nio.svg?logo=liberapay&style=flat)](https://liberapay.com/nio/) 8 | 9 | Nio is an upcoming [Matrix](https://matrix.org) client for iOS. 10 | 11 | For the time being this project is still very much a work in progress. For updates, please check by in our matrix room → [#niochat:matrix.org](https://matrix.to/#/#niochat:matrix.org). 12 | 13 | ~~Want to give it a spin? Join the public [TestFlight Beta](https://testflight.apple.com/join/KlXr3kKz).~~ 14 | 15 | **This project is currently on hiatus and not under active development. Please use one of the other [fantastic iOS Matrix clients](https://matrix.org/ecosystem/clients/) :)** 16 | 17 | ![screenshot](https://nio.chat/screenshots.png) 18 | 19 | ### Getting Started 20 | 21 | Since Nio uses Swift Package Manager all you need to do is clone the project, open it in Xcode and hit R to run the app on your simulator or device. 22 | 23 | ### Translations 24 | 25 | If you can help translate Nio into other languages your help would be much appreciated! Translation files are available on [translate.riot.im](https://translate.riot.im/engage/nio). 26 | 27 | [![Translation status](https://translate.riot.im/widgets/nio/-/nio/multi-auto.svg)](https://translate.riot.im/engage/nio/?utm_source=widget) 28 | 29 | ### Funding 30 | 31 | As with any project, development costs a lot of time and money. Nio has no intention whatsoever of ever becoming a paid application or to support itself through *icky* means like ads or selling user data. But if you feel so inclined to support the project financially, we'd be forever grateful. 32 | 33 | You can either use [Liberapay](https://liberapay.com/nio/) for "traditional" payment methods or donate some [Nano](https://nano.org) to `nano_1dr4k1p63sas5qf9wyepqnuxwpjgik8z1f3nmk1gs8dpopaz97er6rq4tojm`. 34 | 35 | | | 36 | |------| 37 | | Nano | 38 | 39 | Other cryptocurrencies are of course just as welcome, just ask in the chat. 40 | 41 | Thank you! ❤️ 42 | 43 | ### Similar Apps 44 | 45 | You might also have fun checking out the following applications. 46 | 47 | - [FluffyChat](https://fluffychat.im/), arguably the cutest mobile Matrix client. 48 | - [Ditto Chat](https://www.dittochat.org/), cross-platform (based on ReactNative) and just as purple 💜 49 | 50 | ### Why "Nio"? 51 | 52 | *Nio* is short for *Niobe* (albeit pronounced differently), captain of the ship *Logos* in the [Matrix trilogy](https://en.wikipedia.org/wiki/The_Matrix_(franchise)). Additionally it's a homonym of *Neo*, because there's definitely not enough Matrix projects jumping on that pun train already. 53 | 54 | The abbreviation NIO is also commonly used to refer to the term *non-blocking IO*. I find this entertaining for a messaging client, which is essentially a human IO interface. 55 | -------------------------------------------------------------------------------- /Resources/AppIcon/Default/Default.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/Default.sketch -------------------------------------------------------------------------------- /Resources/AppIcon/Default/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/Icon.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/Rounded_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/Rounded_1024.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/Rounded_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/Rounded_500.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_20pt.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_20pt@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_20pt@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_29pt.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_29pt@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_29pt@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_40pt.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_40pt@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_40pt@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_60pt@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_60pt@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_76pt.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_76pt@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Default/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Default/icon_83.5@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Dark/Six Colors Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Dark/Six Colors Dark.png -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Dark/Six Colors Dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Dark/Six Colors Dark@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Dark/Six Colors Dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Dark/Six Colors Dark@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Dark/Six Colors.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Dark/Six Colors.sketch -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Light/Six Colors Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Light/Six Colors Light.png -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Light/Six Colors Light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Light/Six Colors Light@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Six Colors Light/Six Colors Light@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Six Colors Light/Six Colors Light@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Sketch/Sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Sketch/Sketch.png -------------------------------------------------------------------------------- /Resources/AppIcon/Sketch/Sketch@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Sketch/Sketch@2x.png -------------------------------------------------------------------------------- /Resources/AppIcon/Sketch/Sketch@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/AppIcon/Sketch/Sketch@3x.png -------------------------------------------------------------------------------- /Resources/AppIcon/attribution.txt: -------------------------------------------------------------------------------- 1 | chat by mikicon from the Noun Project -------------------------------------------------------------------------------- /Resources/Application.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/Application.sketch -------------------------------------------------------------------------------- /Resources/Base/Chat.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/Base/Chat.pdf -------------------------------------------------------------------------------- /Resources/Base/Chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/Base/Chat.png -------------------------------------------------------------------------------- /Resources/Base/Chat.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niochat/nio/35342d62d37fc3d176f119a54bb75d26fc8e446e/Resources/Base/Chat.sketch -------------------------------------------------------------------------------- /Resources/Base/Chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Resources/Base/attribution.txt: -------------------------------------------------------------------------------- 1 | chat by mikicon from the Noun Project -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | lane :beta do 17 | ensure_git_status_clean 18 | # increment_build_number 19 | build_app(project: "Nio.xcodeproj", scheme: "Nio") 20 | upload_to_testflight( 21 | skip_waiting_for_build_processing: true 22 | ) 23 | end 24 | 25 | after_all do |lane| 26 | notification(message: "finished lane '#{lane}'", open: "http://appstoreconnect.apple.com") 27 | end 28 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ### beta 19 | ``` 20 | fastlane beta 21 | ``` 22 | 23 | 24 | ---- 25 | 26 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 27 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 28 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 29 | -------------------------------------------------------------------------------- /swiftgen.yml: -------------------------------------------------------------------------------- 1 | strings: 2 | inputs: "Nio/Supporting Files/en.lproj" 3 | filter: .+\.strings$ 4 | outputs: 5 | - templateName: structured-swift4 6 | output: Nio/Generated/Strings.swift 7 | 8 | xcassets: 9 | inputs: "Nio/Assets.xcassets" 10 | filter: .+\.xcassets$ 11 | outputs: 12 | - templateName: swift4 13 | output: Nio/Generated/Assets.swift 14 | --------------------------------------------------------------------------------