UserListManagerViewModel.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. //
  2. // UserListManagerViewModel.swift
  3. // TUIRoomKit
  4. //
  5. // Created by janejntang on 2023/2/10.
  6. // Copyright © 2023 Tencent. All rights reserved.
  7. //
  8. import Foundation
  9. import RTCRoomEngine
  10. protocol UserListManagerViewEventResponder: AnyObject {
  11. func makeToast(text: String)
  12. func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?)
  13. func setUserListManagerViewHidden(isHidden: Bool)
  14. func dismissView()
  15. func updateUI(item: ButtonItemData)
  16. func updateStackView(items:[ButtonItemData])
  17. func addStackView(item: ButtonItemData, index: Int?)
  18. func removeStackView(itemType: ButtonItemData.ButtonType)
  19. func showUserNameInputView(userName: String?)
  20. }
  21. class UserListManagerViewModel: NSObject {
  22. var selectUserId: String = ""
  23. let timeoutNumber: Double = 60
  24. var userListManagerItems: [ButtonItemData] = []
  25. weak var viewResponder: UserListManagerViewEventResponder?
  26. var engineManager: EngineManager {
  27. EngineManager.shared
  28. }
  29. var roomInfo: TUIRoomInfo {
  30. engineManager.store.roomInfo
  31. }
  32. var currentUser: UserEntity {
  33. engineManager.store.currentUser
  34. }
  35. var attendeeList: [UserEntity] {
  36. engineManager.store.attendeeList
  37. }
  38. var selectUserInfo: UserEntity? {
  39. attendeeList.first(where: { $0.userId == selectUserId } )
  40. }
  41. init(selectUserId: String) {
  42. self.selectUserId = selectUserId
  43. super.init()
  44. createManagerItems()
  45. subscribeEngine()
  46. }
  47. deinit {
  48. unsubscribeEngine()
  49. debugPrint("self:\(self)")
  50. }
  51. private func subscribeEngine() {
  52. EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
  53. EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
  54. EngineEventCenter.shared.subscribeEngine(event: .onSendMessageForUserDisableChanged, observer: self)
  55. EngineEventCenter.shared.subscribeEngine(event: .onSeatListChanged, observer: self)
  56. EngineEventCenter.shared.subscribeEngine(event: .onUserInfoChanged, observer: self)
  57. }
  58. private func unsubscribeEngine() {
  59. EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
  60. EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
  61. EngineEventCenter.shared.unsubscribeEngine(event: .onSendMessageForUserDisableChanged, observer: self)
  62. EngineEventCenter.shared.unsubscribeEngine(event: .onSeatListChanged, observer: self)
  63. EngineEventCenter.shared.unsubscribeEngine(event: .onUserInfoChanged, observer: self)
  64. }
  65. private func createManagerItems() {
  66. if checkMediaShownState() {
  67. userListManagerItems.append(muteAudioItem)
  68. userListManagerItems.append(muteVideoItem)
  69. }
  70. if checkInviteSeatItemShownState() {
  71. userListManagerItems.append(inviteSeatItem)
  72. }
  73. if checkChangeHostItemShownState() {
  74. userListManagerItems.append(changeHostItem)
  75. }
  76. if checkSetAdministratorItemShownState() {
  77. userListManagerItems.append(setAdministratorItem)
  78. }
  79. if checkModifyTheNameItemShownState() {
  80. userListManagerItems.append(modifyTheNameItem)
  81. }
  82. if checkMuteMessageItemShownState() {
  83. userListManagerItems.append(muteMessageItem)
  84. }
  85. if checkKickOutItemShownState() {
  86. userListManagerItems.append(kickOutItem)
  87. }
  88. }
  89. private lazy var muteAudioItem: ButtonItemData = {
  90. let item = ButtonItemData()
  91. item.normalTitle = .muteText
  92. item.normalIcon = "room_unMute_audio"
  93. item.selectedTitle = .requestOpenAudioText
  94. item.selectedIcon = "room_mute_audio"
  95. item.resourceBundle = tuiRoomKitBundle()
  96. item.buttonType = .muteAudioItemType
  97. item.isSelect = !(selectUserInfo?.hasAudioStream ?? true)
  98. item.hasLineView = true
  99. item.action = { [weak self] sender in
  100. guard let self = self, let button = sender as? UIButton else { return }
  101. self.muteAudioAction(sender: button)
  102. }
  103. return item
  104. }()
  105. private lazy var muteVideoItem: ButtonItemData = {
  106. let item = ButtonItemData()
  107. item.normalTitle = .closeVideoText
  108. item.normalIcon = "room_unMute_video"
  109. item.selectedTitle = .requestOpenVideoText
  110. item.selectedIcon = "room_mute_video"
  111. item.resourceBundle = tuiRoomKitBundle()
  112. item.buttonType = .muteVideoItemType
  113. item.isSelect = !(selectUserInfo?.hasVideoStream ?? true)
  114. item.hasLineView = true
  115. item.action = { [weak self] sender in
  116. guard let self = self, let button = sender as? UIButton else { return }
  117. self.muteVideoAction(sender: button)
  118. }
  119. return item
  120. }()
  121. private lazy var inviteSeatItem: ButtonItemData = {
  122. let item = ButtonItemData()
  123. item.normalTitle = .inviteSeatText
  124. item.normalIcon = "room_invite_seat"
  125. item.selectedTitle = .stepDownSeatText
  126. item.selectedIcon = "room_step_down_seat"
  127. item.resourceBundle = tuiRoomKitBundle()
  128. item.isSelect = selectUserInfo?.isOnSeat ?? false
  129. item.hasLineView = true
  130. item.action = { [weak self] sender in
  131. guard let self = self, let button = sender as? UIButton else { return }
  132. self.inviteSeatAction(sender: button)
  133. }
  134. return item
  135. }()
  136. private lazy var changeHostItem: ButtonItemData = {
  137. let item = ButtonItemData()
  138. item.normalTitle = .changeHostText
  139. item.normalIcon = "room_change_host"
  140. item.resourceBundle = tuiRoomKitBundle()
  141. item.hasLineView = true
  142. item.action = { [weak self] sender in
  143. guard let self = self, let button = sender as? UIButton else { return }
  144. self.changeHostAction(sender: button)
  145. }
  146. return item
  147. }()
  148. private lazy var muteMessageItem: ButtonItemData = {
  149. let item = ButtonItemData()
  150. item.normalTitle = .muteMessageText
  151. item.normalIcon = "room_mute_message"
  152. item.selectedTitle = .unMuteMessageText
  153. item.selectedIcon = "room_unMute_message"
  154. item.resourceBundle = tuiRoomKitBundle()
  155. item.isSelect = selectUserInfo?.disableSendingMessage ?? false
  156. item.hasLineView = true
  157. item.action = { [weak self] sender in
  158. guard let self = self, let button = sender as? UIButton else { return }
  159. self.muteMessageAction(sender: button)
  160. }
  161. return item
  162. }()
  163. private lazy var kickOutItem: ButtonItemData = {
  164. let item = ButtonItemData()
  165. item.normalTitle = .kickOutRoomText
  166. item.normalIcon = "room_kickOut_room"
  167. item.resourceBundle = tuiRoomKitBundle()
  168. item.hasLineView = true
  169. item.action = { [weak self] sender in
  170. guard let self = self, let button = sender as? UIButton else { return }
  171. self.kickOutAction(sender: button)
  172. }
  173. return item
  174. }()
  175. private lazy var setAdministratorItem: ButtonItemData = {
  176. let item = ButtonItemData()
  177. item.normalTitle = .setAsAdministratorText
  178. item.selectedTitle = .undoAdministratorText
  179. item.normalIcon = "room_set_administrator"
  180. item.selectedIcon = "room_undo_administrator"
  181. item.resourceBundle = tuiRoomKitBundle()
  182. item.isSelect = selectUserInfo?.userRole == .administrator
  183. item.hasLineView = true
  184. item.action = { [weak self] sender in
  185. guard let self = self, let button = sender as? UIButton else { return }
  186. self.setAdministratorAction(sender: button)
  187. }
  188. return item
  189. }()
  190. private lazy var modifyTheNameItem: ButtonItemData = {
  191. let item = ButtonItemData()
  192. item.normalTitle = .modifyTheName
  193. item.normalIcon = "room_modify_name"
  194. item.resourceBundle = tuiRoomKitBundle()
  195. item.hasLineView = true
  196. item.action = { [weak self] sender in
  197. guard let self = self, let button = sender as? UIButton else { return }
  198. self.modifyTheNameAction(sender: button)
  199. }
  200. return item
  201. }()
  202. func backBlockAction(sender: UIView) {
  203. sender.isHidden = true
  204. }
  205. private func checkMediaShownState() -> Bool {
  206. guard selectUserId != currentUser.userId else { return false }
  207. if !roomInfo.isSeatEnabled {
  208. return true
  209. } else if let selectUserInfo = selectUserInfo, selectUserInfo.isOnSeat {
  210. return true
  211. } else {
  212. return false
  213. }
  214. }
  215. private func checkInviteSeatItemShownState() -> Bool {
  216. return roomInfo.isSeatEnabled && selectUserId != currentUser.userId
  217. }
  218. private func checkChangeHostItemShownState() -> Bool {
  219. return currentUser.userRole == .roomOwner && selectUserId != currentUser.userId
  220. }
  221. private func checkMuteMessageItemShownState() -> Bool {
  222. return selectUserId != currentUser.userId
  223. }
  224. private func checkKickOutItemShownState() -> Bool {
  225. return currentUser.userRole == .roomOwner && selectUserId != currentUser.userId
  226. }
  227. private func checkSetAdministratorItemShownState() -> Bool {
  228. return currentUser.userRole == .roomOwner && selectUserId != currentUser.userId
  229. }
  230. private func checkModifyTheNameItemShownState() -> Bool {
  231. return true
  232. }
  233. }
  234. extension UserListManagerViewModel {
  235. private func addViewItem(buttonItem: ButtonItemData, index: Int) {
  236. guard !isContainedViewItem(buttonType: buttonItem.buttonType) else { return }
  237. if userListManagerItems.count > index + 1 {
  238. userListManagerItems.insert(buttonItem, at: index)
  239. viewResponder?.addStackView(item: buttonItem, index: index)
  240. } else {
  241. userListManagerItems.append(buttonItem)
  242. viewResponder?.addStackView(item: buttonItem, index: nil)
  243. }
  244. }
  245. private func removeViewItem(buttonType: ButtonItemData.ButtonType) {
  246. userListManagerItems.removeAll(where: { $0.buttonType == buttonType })
  247. viewResponder?.removeStackView(itemType: buttonType)
  248. }
  249. private func isContainedViewItem(buttonType: ButtonItemData.ButtonType) -> Bool {
  250. return userListManagerItems.contains(where: { $0.buttonType == buttonType })
  251. }
  252. private func muteAudioAction(sender: UIButton) {
  253. guard let userInfo = attendeeList.first(where: { $0.userId == selectUserId }) else { return }
  254. if userInfo.hasAudioStream {
  255. engineManager.closeRemoteDeviceByAdmin(userId: selectUserId, device: .microphone)
  256. } else {
  257. engineManager.openRemoteDeviceByAdmin(userId: selectUserId, device: .microphone)
  258. viewResponder?.makeToast(text: .invitedOpenAudioText)
  259. }
  260. viewResponder?.dismissView()
  261. }
  262. private func muteVideoAction(sender: UIButton) {
  263. guard let userInfo = selectUserInfo else { return }
  264. if userInfo.hasVideoStream {
  265. engineManager.closeRemoteDeviceByAdmin(userId: selectUserId, device: .camera)
  266. } else {
  267. engineManager.openRemoteDeviceByAdmin(userId: selectUserId, device: .camera)
  268. viewResponder?.makeToast(text: .invitedOpenVideoText)
  269. }
  270. viewResponder?.dismissView()
  271. }
  272. func inviteSeatAction(sender: UIButton) {
  273. guard let userInfo = selectUserInfo else { return }
  274. sender.isSelected = !sender.isSelected
  275. if sender.isSelected {
  276. guard engineManager.store.seatList.count < roomInfo.maxSeatCount else {
  277. RoomRouter.makeToastInCenter(toast: .theStageIsFullText, duration: 0.5)
  278. viewResponder?.dismissView()
  279. return
  280. }
  281. engineManager.takeUserOnSeatByAdmin(userId: selectUserId, timeout: timeoutNumber) { _,_ in
  282. let text: String = localizedReplace(.onStageText, replace: userInfo.userName)
  283. RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
  284. } onRejected: { requestId, userId, message in
  285. let text: String = localizedReplace(.refusedTakeSeatInvitationText, replace: userInfo.userName)
  286. RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
  287. } onCancelled: { _, _ in
  288. } onTimeout: { _, _ in
  289. let text: String = localizedReplace(.takeSeatInvitationTimeoutText, replace: userInfo.userName)
  290. RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
  291. } onError: { _, _, code, message in
  292. let text: String = code == .requestIdRepeat ? .receivedSameRequestText : message
  293. RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
  294. }
  295. viewResponder?.makeToast(text: .invitedTakeSeatText)
  296. } else {
  297. engineManager.kickUserOffSeatByAdmin(userId: selectUserId)
  298. }
  299. viewResponder?.dismissView()
  300. }
  301. private func changeHostAction(sender: UIButton) {
  302. guard let userInfo = attendeeList.first(where: { $0.userId == selectUserId }) else { return }
  303. let title = localizedReplace(.transferHostTitle, replace: userInfo.userName)
  304. viewResponder?.showAlert(title: title, message: .transferHostMessage, sureTitle: .transferHostsureText, declineTitle: .cancelText, sureBlock: { [weak self] in
  305. guard let self = self else { return }
  306. if self.roomInfo.isScreenShareDisableForAllUser, self.currentUser.hasScreenStream {
  307. self.engineManager.stopScreenCapture()
  308. }
  309. self.engineManager.changeUserRole(userId: self.selectUserId, role: .roomOwner) { [weak self] in
  310. guard let self = self else { return }
  311. let text = localizedReplace(.haveTransferredMasterText, replace: userInfo.userName)
  312. self.viewResponder?.makeToast(text: text)
  313. self.viewResponder?.dismissView()
  314. } onError: { [weak self] code, message in
  315. guard let self = self else { return }
  316. self.viewResponder?.dismissView()
  317. }
  318. }, declineBlock: nil)
  319. }
  320. private func muteMessageAction(sender: UIButton) {
  321. guard let userInfo = attendeeList.first(where: { $0.userId == selectUserId }) else { return }
  322. engineManager.disableSendingMessageByAdmin(userId: selectUserId, isDisable: !userInfo.disableSendingMessage)
  323. viewResponder?.dismissView()
  324. }
  325. func kickOutAction(sender: UIButton) {
  326. let kickOutTitle = localizedReplace(.kickOutText, replace: selectUserInfo?.userName ?? "")
  327. viewResponder?.showAlert(title: kickOutTitle, message: nil, sureTitle: .alertOkText, declineTitle: .cancelText, sureBlock: { [weak self] in
  328. guard let self = self else { return }
  329. self.engineManager.kickRemoteUserOutOfRoom(userId: self.selectUserId)
  330. self.viewResponder?.dismissView()
  331. }, declineBlock: nil)
  332. }
  333. private func setAdministratorAction(sender: UIButton) {
  334. guard let userInfo = selectUserInfo else { return }
  335. let role: TUIRole = userInfo.userRole == .administrator ? .generalUser : .administrator
  336. engineManager.changeUserRole(userId: selectUserId, role: role) { [weak self] in
  337. guard let self = self else { return }
  338. let setAdministratorText = localizedReplace(.setUpAdministratorText, replace: userInfo.userName)
  339. let removeAdministratorText = localizedReplace(.removedAdministratorText, replace: userInfo.userName)
  340. let text: String = role == .administrator ? setAdministratorText : removeAdministratorText
  341. self.viewResponder?.makeToast(text: text)
  342. if role == .generalUser, self.roomInfo.isScreenShareDisableForAllUser, userInfo.hasScreenStream {
  343. self.engineManager.closeRemoteDeviceByAdmin(userId: userInfo.userId, device: .screenSharing)
  344. }
  345. self.viewResponder?.dismissView()
  346. } onError: { [weak self] code, message in
  347. guard let self = self else { return }
  348. self.viewResponder?.dismissView()
  349. debugPrint("changeUserRole,code:\(code),message:\(message)")
  350. }
  351. }
  352. private func modifyTheNameAction(sender: UIButton) {
  353. viewResponder?.showUserNameInputView(userName: selectUserInfo?.userName)
  354. }
  355. }
  356. extension UserListManagerViewModel: RoomEngineEventResponder {
  357. func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
  358. switch name {
  359. case .onUserAudioStateChanged:
  360. guard let userId = param?["userId"] as? String else { return }
  361. guard let hasAudio = param?["hasAudio"] as? Bool else { return }
  362. guard userId == selectUserId else { return }
  363. muteAudioItem.isSelect = !hasAudio
  364. viewResponder?.updateUI(item: muteAudioItem)
  365. case .onUserVideoStateChanged:
  366. guard let userId = param?["userId"] as? String else { return }
  367. guard let hasVideo = param?["hasVideo"] as? Bool else { return }
  368. guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
  369. guard userId == selectUserId, streamType == .cameraStream else { return }
  370. muteVideoItem.isSelect = !hasVideo
  371. viewResponder?.updateUI(item: muteVideoItem)
  372. case .onSendMessageForUserDisableChanged:
  373. guard let userId = param?["userId"] as? String else { return }
  374. guard let muted = param?["muted"] as? Bool else { return }
  375. guard userId == selectUserId else { return }
  376. muteMessageItem.isSelect = muted
  377. viewResponder?.updateUI(item: muteMessageItem)
  378. case .onSeatListChanged:
  379. if let seated = param?["seated"] as? [TUISeatInfo], seated.first(where: { $0.userId == selectUserId }) != nil {
  380. inviteSeatItem.isSelect = true
  381. viewResponder?.updateUI(item: inviteSeatItem)
  382. addViewItem(buttonItem: muteAudioItem, index: 0)
  383. addViewItem(buttonItem: muteVideoItem, index: 1)
  384. }
  385. if let left = param?["left"] as? [TUISeatInfo], left.first(where: { $0.userId == selectUserId }) != nil {
  386. inviteSeatItem.isSelect = false
  387. viewResponder?.updateUI(item: inviteSeatItem)
  388. removeViewItem(buttonType: .muteAudioItemType)
  389. removeViewItem(buttonType: .muteVideoItemType)
  390. }
  391. case .onUserInfoChanged:
  392. guard let modifyFlag = param?["modifyFlag"] as? TUIUserInfoModifyFlag, modifyFlag.contains(.userRole) else { return }
  393. guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
  394. guard userInfo.userId == currentUser.userId || userInfo.userId == selectUserId else { return }
  395. guard currentUser.userId != selectUserId else { return }
  396. guard let selectUserInfo = selectUserInfo else { return }
  397. userListManagerItems.removeAll()
  398. if currentUser.userRole == .roomOwner {
  399. createManagerItems()
  400. viewResponder?.updateStackView(items: userListManagerItems)
  401. } else if currentUser.userRole == .administrator, selectUserInfo.userRole == .generalUser {
  402. createManagerItems()
  403. viewResponder?.updateStackView(items: userListManagerItems)
  404. } else {
  405. viewResponder?.dismissView()
  406. }
  407. default: break
  408. }
  409. }
  410. }
  411. extension UserListManagerViewModel: UserNameInputViewResponder {
  412. func changeUserName(name: String) {
  413. engineManager.changeUserNameCard(userid: selectUserId, nameCard: name) { [weak self] in
  414. guard let self = self else { return }
  415. self.viewResponder?.makeToast(text: .nameChangedSuccessfully)
  416. } onError: { [weak self] _, _ in
  417. guard let self = self else { return }
  418. self.viewResponder?.makeToast(text: .nameChangeFailed)
  419. }
  420. }
  421. func onEndEditing() {
  422. viewResponder?.dismissView()
  423. }
  424. }
  425. private extension String {
  426. static var muteText: String {
  427. localized("Mute")
  428. }
  429. static var requestOpenVideoText: String {
  430. localized("Ask to start video")
  431. }
  432. static var requestOpenAudioText: String {
  433. localized("Ask to unmute")
  434. }
  435. static var closeVideoText: String {
  436. localized("Stop video")
  437. }
  438. static var changeHostText: String {
  439. localized("Make host")
  440. }
  441. static var muteMessageText: String {
  442. localized("Disable chat")
  443. }
  444. static var unMuteMessageText: String {
  445. localized("Enable chat")
  446. }
  447. static var kickOutRoomText: String {
  448. localized("Remove")
  449. }
  450. static var stepDownSeatText: String {
  451. localized("Leave the stage")
  452. }
  453. static var inviteSeatText: String {
  454. localized("Invite to stage")
  455. }
  456. static var invitedTakeSeatText: String {
  457. localized("The audience has been invited to the stage")
  458. }
  459. static var refusedTakeSeatInvitationText: String {
  460. localized("xx refused to go on stage")
  461. }
  462. static var takeSeatInvitationTimeoutText: String {
  463. localized("The invitation to xx to go on stage has timed out")
  464. }
  465. static var invitedOpenAudioText: String {
  466. localized("The audience has been invited to open the audio")
  467. }
  468. static var invitedOpenVideoText: String {
  469. localized("The audience has been invited to open the video")
  470. }
  471. static var kickOutText: String {
  472. localized("Do you want to move xx out of the conference?")
  473. }
  474. static var setAsAdministratorText: String {
  475. localized("Set as administrator")
  476. }
  477. static var undoAdministratorText: String {
  478. localized("Undo administrator")
  479. }
  480. static var haveTransferredMasterText: String {
  481. localized("The host has been transferred to xx")
  482. }
  483. static var setUpAdministratorText: String {
  484. localized("xx has been set as conference admin")
  485. }
  486. static var removedAdministratorText: String {
  487. localized("The conference admin status of xx has been withdrawn")
  488. }
  489. static var alertOkText: String {
  490. localized("OK")
  491. }
  492. static var cancelText: String {
  493. localized("Cancel")
  494. }
  495. static var transferHostTitle: String {
  496. localized("Transfer the host to xx")
  497. }
  498. static var transferHostMessage: String {
  499. localized("After transfer the host, you will become a general user")
  500. }
  501. static var transferHostsureText: String {
  502. localized("Confirm transfer")
  503. }
  504. static var receivedSameRequestText: String {
  505. localized("This member has already received the same request, please try again later")
  506. }
  507. static var onStageText: String {
  508. localized("xx is on stage")
  509. }
  510. static var theStageIsFullText: String {
  511. localized("The stage is full")
  512. }
  513. static let modifyTheName = localized("Modify the name")
  514. static let nameChangedSuccessfully = localized("Name changed successfully")
  515. static let nameChangeFailed = localized("Name change failed")
  516. }