Change license to AGPL
This commit:
- Updates the `LICENSE` file
- Start every file with something like:
// Copyright YEAR_FIRST_PUBLISHED Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
---
First, I removed existing license headers with this Ruby 3.1.2 script:
require 'set'
EXTENSIONS_TO_CHECK = Set['.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift']
same = 0
different = 0
all_files = `git ls-files`.lines.map { |line| line.strip }
all_files.each do |relative_path|
if relative_path == 'Pods'
next
end
unless EXTENSIONS_TO_CHECK.include? File.extname(relative_path)
next
end
path = File.expand_path(relative_path)
contents = File.read(path)
new_contents = contents.sub(/\/\/\n\/\/ Copyright .*\n\/\/\n\n/, '')
if contents == new_contents
same += 1
else
different += 1
end
File.write(path, new_contents)
end
puts "updated #{different} file(s), left #{same} untouched"
I'm sure this script could be improved, but it worked well enough.
Then, I created `Scripts/lint/lint-license-headers` and ran it to auto-
fix a lot of files. This changed the mode of some files, but I think
that's actually desirable. For example,
`SignalServiceKit/src/Util/AppContext.m` previously had a mode of
`0755/-rwxr-xr-x`, and it's now `0644/-rw-r--r--`.
Then I fixed some stragglers and updated the precommit script.
See [a similar change in the Desktop app][0].
[0]: 8bfaf598af
383 lines
16 KiB
Swift
383 lines
16 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
@testable import SignalServiceKit
|
|
import XCTest
|
|
|
|
class NSAttributedStringInGroupUpdateCopyTest: SSKBaseTestSwift {
|
|
private func assertEqual(_ one: NSAttributedString, _ two: NSAttributedString) {
|
|
XCTAssertEqual(one, two)
|
|
}
|
|
|
|
func test_XMadeYAnAdmin() {
|
|
assertEqual(.aliceMadeBobAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .bob))
|
|
assertEqual(.aliceHMadeAliceAdmin, NSAttributedString.make(firstUser: .aliceH, madeAdmin: .alice))
|
|
assertEqual(.aliceMadeMadeAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .made))
|
|
assertEqual(.aliceMadeAnotherAliceAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .anotherAlice))
|
|
assertEqual(.aliceMadeSingleArgFormatSpecifierAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .singleArgFormatSpecifier))
|
|
assertEqual(.aliceMadeMultiArgFormatSpecifierAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .multiArgFormatSpecifier))
|
|
assertEqual(.aliceMadeEmptyAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .empty))
|
|
}
|
|
|
|
func test_XMadeYAnAdmin_ArabicNameInEnglishString() {
|
|
assertEqual(.aliceMadeFatimaInArabicAnAdmin, NSAttributedString.make(firstUser: .alice, madeAdmin: .fatima))
|
|
}
|
|
|
|
func test_XMadeYAnAdmin_EnglishNameInArabicString() {
|
|
assertEqual(.aliceMadeBobAdmin_Arabic, NSAttributedString.makeArabic(firstUser: .alice, madeAdmin: .bob))
|
|
}
|
|
|
|
func test_XMadeYAnAdmin_ChineseNameInEnglishString() {
|
|
assertEqual(.qiInChineseMadeAliceAdmin, NSAttributedString.make(firstUser: .qi, madeAdmin: .alice))
|
|
}
|
|
|
|
func test_XMadeYAnAdmin_ChineseAndArabicNamesInArabicString() {
|
|
assertEqual(.qiInChineseMadeFatimaInArabicAdmin_Arabic, NSAttributedString.makeArabic(firstUser: .qi, madeAdmin: .fatima))
|
|
}
|
|
|
|
/// Note that this test involves formatting numbers into a format string,
|
|
/// which is done in the guts of Signal using the "current locale".
|
|
/// Therefore, running this test outside of the "United States" locale in
|
|
/// English will fail due to substitution errors.
|
|
func test_XInvitedNPeople() throws {
|
|
try XCTSkipIf(Locale.current.identifier != "en_US", "This test requires the en_US locale for number formatting.")
|
|
|
|
assertEqual(.aliceInvitedOnePerson, NSAttributedString.make(user: .alice, invitedNPeople: 1))
|
|
assertEqual(.aliceInvitedTwoPeople, NSAttributedString.make(user: .alice, invitedNPeople: 2))
|
|
assertEqual(.aliceInvitedOneMillionPeople, NSAttributedString.make(user: .alice, invitedNPeople: 1000000))
|
|
assertEqual(.emptyInvitedThreePeople, NSAttributedString.make(user: .empty, invitedNPeople: 3))
|
|
}
|
|
|
|
/// Technically this tests a string that is never used in group updates.
|
|
/// However, we don't have any relevant strings that have the name at the
|
|
/// end of the string, and I wanted to test that.
|
|
func test_NameAtEndOfString() {
|
|
assertEqual(
|
|
.aliceSomethingWithBobsNameAtEndOfString,
|
|
NSAttributedString.makeWithNameAtEndOfString(firstUser: .alice, userAtEndOfString: .bob, lang: "en")
|
|
)
|
|
|
|
assertEqual(
|
|
.aliceSomethingWithBobsNameAtEndOfString_Arabic,
|
|
NSAttributedString.makeWithNameAtEndOfString(firstUser: .alice, userAtEndOfString: .bob, lang: "ar")
|
|
)
|
|
}
|
|
|
|
/// Some localized strings invert the "expected order" of format args, e.g.
|
|
/// `%2$@` may be placed before `%1$@` for a given language simply based on
|
|
/// the grammar of that language. This tests for that, using a string that
|
|
/// is known to have this behavior in Malayalam.
|
|
func test_LocalizedStringInvertsFormatArgOrder() {
|
|
assertEqual(
|
|
.aliceSomethingBobWithInvertedNameOrder,
|
|
NSAttributedString.makeWithInvertedFormatArgOrder(firstUser: .alice, secondUser: .bob)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - "X made Y an admin"
|
|
|
|
private extension NSAttributedString {
|
|
static let aliceMadeBobAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .bob) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.bob, toRange: NSRange(location: 13, length: 5))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceHMadeAliceAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .aliceH) made \(user: .alice) an admin.")
|
|
mutable.addAddress(.aliceH, toRange: NSRange(location: 0, length: 9))
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 15, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceMadeMadeAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .made) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.made, toRange: NSRange(location: 13, length: 6))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceMadeAnotherAliceAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .anotherAlice) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.anotherAlice, toRange: NSRange(location: 13, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceMadeSingleArgFormatSpecifierAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .singleArgFormatSpecifier) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.singleArgFormatSpecifier, toRange: NSRange(location: 13, length: 4))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceMadeMultiArgFormatSpecifierAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .multiArgFormatSpecifier) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.multiArgFormatSpecifier, toRange: NSRange(location: 13, length: 6))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
/// This should theoretically be impossible - we should never in practice
|
|
/// have an empty substitution. However, if we do, we don't want to fail
|
|
/// here :)
|
|
static let aliceMadeEmptyAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .empty) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.empty, toRange: NSRange(location: 13, length: 2))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static func make(firstUser: ReferencedUser, madeAdmin secondUser: ReferencedUser) -> NSAttributedString {
|
|
make(
|
|
fromFormat: .xMadeYAnAdminFormat,
|
|
groupUpdateFormatArgs: [
|
|
.name(firstUser.name, firstUser.address),
|
|
.name(secondUser.name, secondUser.address)
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - "X made Y an admin", but using non-English languages
|
|
|
|
private extension NSAttributedString {
|
|
static let aliceMadeBobAdmin_Arabic: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "إنَّ \(user: .alice) قد جعلَ \(user: .bob) مُشرفاً.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 5, length: 7))
|
|
mutable.addAddress(.bob, toRange: NSRange(location: 21, length: 5))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceMadeFatimaInArabicAnAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) made \(user: .fatima) an admin.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.fatima, toRange: NSRange(location: 13, length: 10))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let qiInChineseMadeAliceAdmin: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .qi) made \(user: .alice) an admin.")
|
|
mutable.addAddress(.qi, toRange: NSRange(location: 0, length: 3))
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 9, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let qiInChineseMadeFatimaInArabicAdmin_Arabic: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "إنَّ \(user: .qi) قد جعلَ \(user: .fatima) مُشرفاً.")
|
|
mutable.addAddress(.qi, toRange: NSRange(location: 5, length: 3))
|
|
mutable.addAddress(.fatima, toRange: NSRange(location: 17, length: 10))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static func makeArabic(firstUser: ReferencedUser, madeAdmin secondUser: ReferencedUser) -> NSAttributedString {
|
|
make(
|
|
fromFormat: .xMadeYAnAdminFormat_Arabic,
|
|
groupUpdateFormatArgs: [
|
|
.name(firstUser.name, firstUser.address),
|
|
.name(secondUser.name, secondUser.address)
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - "X invited N people"
|
|
|
|
private extension NSAttributedString {
|
|
static let aliceInvitedOnePerson: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 1 person to the group.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceInvitedTwoPeople: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 2 people to the group.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceInvitedOneMillionPeople: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) invited 1,000,000 people to the group.")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let emptyInvitedThreePeople: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .empty) invited 3 people to the group.")
|
|
mutable.addAddress(.empty, toRange: NSRange(location: 0, length: 2))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static func make(user: ReferencedUser, invitedNPeople: Int) -> NSAttributedString {
|
|
make(
|
|
fromFormat: .xInvitedNPeopleFormat,
|
|
groupUpdateFormatArgs: [.raw(invitedNPeople), .name(user.name, user.address)]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Name at end of string
|
|
|
|
private extension NSAttributedString {
|
|
static let aliceSomethingWithBobsNameAtEndOfString: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) to \(user: .bob)")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.bob, toRange: NSRange(location: 11, length: 5))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static let aliceSomethingWithBobsNameAtEndOfString_Arabic: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "\(user: .alice) إلى \(user: .bob)")
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 0, length: 7))
|
|
mutable.addAddress(.bob, toRange: NSRange(location: 12, length: 5))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
static func makeWithNameAtEndOfString(
|
|
firstUser: ReferencedUser,
|
|
userAtEndOfString otherUser: ReferencedUser,
|
|
lang: String
|
|
) -> NSAttributedString {
|
|
make(
|
|
fromFormat: String.newGroupMessageNotificationTitleFormat(lang: lang),
|
|
groupUpdateFormatArgs: [.name(firstUser.name, firstUser.address), .name(otherUser.name, otherUser.address)]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Localized string inverts format arg order
|
|
|
|
private extension NSAttributedString {
|
|
static let aliceSomethingBobWithInvertedNameOrder: NSAttributedString = {
|
|
let mutable = NSMutableAttributedString(string: "“\(user: .bob)”-এ “\(user: .alice)” যোগ করবেন?")
|
|
mutable.addAddress(.bob, toRange: NSRange(location: 1, length: 5))
|
|
mutable.addAddress(.alice, toRange: NSRange(location: 11, length: 7))
|
|
|
|
return mutable
|
|
}()
|
|
|
|
/// The returned string formats the "second" user passed before the "first"
|
|
/// user, simply as a result of the format string.
|
|
static func makeWithInvertedFormatArgOrder(firstUser: ReferencedUser, secondUser: ReferencedUser) -> NSAttributedString {
|
|
make(
|
|
fromFormat: .addToGroupActionSheetMessage_Bangla,
|
|
groupUpdateFormatArgs: [.name(firstUser.name, firstUser.address), .name(secondUser.name, secondUser.address)]
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Add addresses
|
|
|
|
private extension NSMutableAttributedString {
|
|
func addAddress(_ address: SignalServiceAddress, toRange range: NSRange) {
|
|
addAttribute(.addressOfName, value: address, range: range)
|
|
}
|
|
}
|
|
|
|
// MARK: - Users
|
|
|
|
private extension SignalServiceAddress {
|
|
static let alice = SignalServiceAddress(phoneNumber: "+17735550100")
|
|
static let aliceH = SignalServiceAddress(phoneNumber: "+17735550101")
|
|
static let bob = SignalServiceAddress(phoneNumber: "+17735550102")
|
|
static let made = SignalServiceAddress(phoneNumber: "+17735550103")
|
|
static let anotherAlice = SignalServiceAddress(phoneNumber: "+17735550104")
|
|
static let singleArgFormatSpecifier = SignalServiceAddress(phoneNumber: "+17735550105")
|
|
static let multiArgFormatSpecifier = SignalServiceAddress(phoneNumber: "+17735550106")
|
|
static let empty = SignalServiceAddress(phoneNumber: "+17735550107")
|
|
static let fatima = SignalServiceAddress(phoneNumber: "+17735550108")
|
|
static let qi = SignalServiceAddress(phoneNumber: "+17735550109")
|
|
}
|
|
|
|
private struct ReferencedUser {
|
|
static let alice: ReferencedUser = ReferencedUser(name: "Alice", address: .alice)
|
|
static let aliceH: ReferencedUser = ReferencedUser(name: "Alice H", address: .aliceH)
|
|
static let bob: ReferencedUser = ReferencedUser(name: "Bob", address: .bob)
|
|
static let made: ReferencedUser = ReferencedUser(name: "made", address: .made)
|
|
static let anotherAlice: ReferencedUser = ReferencedUser(name: "Alice", address: .anotherAlice)
|
|
static let singleArgFormatSpecifier: ReferencedUser = ReferencedUser(name: "%@", address: .singleArgFormatSpecifier)
|
|
static let multiArgFormatSpecifier: ReferencedUser = ReferencedUser(name: "%1$@", address: .multiArgFormatSpecifier)
|
|
static let empty: ReferencedUser = ReferencedUser(name: "", address: .empty)
|
|
static let fatima: ReferencedUser = ReferencedUser(name: "فَاطِمَة", address: .fatima)
|
|
static let qi: ReferencedUser = ReferencedUser(name: "琦", address: .qi)
|
|
|
|
let name: String
|
|
let address: SignalServiceAddress
|
|
}
|
|
|
|
private extension String.StringInterpolation {
|
|
/// We wrap all formatted names in Unicode isolates.
|
|
mutating func appendInterpolation(user: ReferencedUser) {
|
|
appendLiteral("\u{2068}\(user.name)\u{2069}")
|
|
}
|
|
}
|
|
|
|
// MARK: - Localized format strings
|
|
|
|
/// We should ideally use ``OWSLocalizedString`` for this, but it doesn't
|
|
/// support specifying the locale (uses the current).
|
|
private extension String {
|
|
static let xMadeYAnAdminFormat: String = {
|
|
try! localized(key: "GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT")
|
|
}()
|
|
|
|
static let xMadeYAnAdminFormat_Arabic: String = {
|
|
try! localized(key: "GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", lang: "ar")
|
|
}()
|
|
|
|
static let xInvitedNPeopleFormat: String = {
|
|
try! localized(key: "GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_%d", tableName: "PluralAware")
|
|
}()
|
|
|
|
static func newGroupMessageNotificationTitleFormat(lang: String) -> String {
|
|
try! localized(key: "NEW_GROUP_MESSAGE_NOTIFICATION_TITLE", lang: lang)
|
|
}
|
|
|
|
static let addToGroupActionSheetMessage_Bangla: String = {
|
|
try! localized(key: "ADD_TO_GROUP_ACTION_SHEET_MESSAGE_FORMAT", lang: "bn")
|
|
}()
|
|
|
|
static func localized(
|
|
key: String,
|
|
tableName: String? = nil,
|
|
lang: String = "en"
|
|
) throws -> String {
|
|
guard
|
|
let bundlePath = Bundle.main.path(forResource: lang, ofType: "lproj"),
|
|
let bundle = Bundle(path: bundlePath)
|
|
else {
|
|
throw OWSAssertionError("Failed to find bundle for lang code \(lang)")
|
|
}
|
|
|
|
return NSLocalizedString(
|
|
key,
|
|
tableName: tableName,
|
|
bundle: bundle,
|
|
value: "",
|
|
comment: ""
|
|
)
|
|
}
|
|
}
|