EditorGroup.swift 488 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068
  1. //
  2. // EditorGroup.swift
  3. // Qmera
  4. //
  5. // Created by Akhmad Al Qindi Irsyam on 20/09/21.
  6. //
  7. import UIKit
  8. import AVKit
  9. import AVFoundation
  10. import QuickLook
  11. import Photos
  12. import NotificationBannerSwift
  13. import nuSDKService
  14. import SwiftLinkPreview
  15. import SDWebImage
  16. import PhotosUI
  17. public class EditorGroup: UIViewController, CLLocationManagerDelegate {
  18. @IBOutlet var wallpaperView: UIImageView!
  19. @IBOutlet var viewButton: UIView!
  20. @IBOutlet var constraintViewTextField: NSLayoutConstraint!
  21. @IBOutlet var buttonVoice: UIButton!
  22. @IBOutlet var buttonSendImage: UIButton!
  23. @IBOutlet var buttonSendPhoto: UIButton!
  24. @IBOutlet var buttonSendSticker: UIButton!
  25. @IBOutlet var buttonSendFile: UIButton!
  26. @IBOutlet var textFieldSend: CustomTextView!
  27. @IBOutlet var heightTextFieldSend: NSLayoutConstraint!
  28. @IBOutlet var buttonSendChat: UIButton!
  29. @IBOutlet var tableChatView: UITableView!
  30. @IBOutlet var constraintTopTextField: NSLayoutConstraint!
  31. @IBOutlet var constraintBottomAttachment: NSLayoutConstraint!
  32. @IBOutlet var viewTextfield: UIView!
  33. @IBOutlet weak var buttonAckConfidential: UIButton!
  34. @IBOutlet weak var constraintBottomTableViewWithTextfield: NSLayoutConstraint!
  35. @IBOutlet weak var viewAttachment: UIStackView!
  36. @IBOutlet weak var tableMention: UITableView!
  37. @IBOutlet weak var heightTableMention: NSLayoutConstraint!
  38. @IBOutlet weak var contraintBottomMention: NSLayoutConstraint!
  39. public var dataGroup: [String: Any?] = [:]
  40. public var dataTopic: [String: Any?] = [:]
  41. var dataMessages: [[String: Any?]] = []
  42. var dataDates: [String] = []
  43. public var dataMessageForward: [[String: Any?]]?
  44. var imageVideoPicker: ImageVideoPicker!
  45. var documentPicker: DocumentPicker!
  46. var currentIndexpath: IndexPath?
  47. var previewItem: NSURL?
  48. var reffId: String?
  49. var stickers = [String]()
  50. public var unique_l_pin = ""
  51. public var fromNotification = false
  52. public var referenceMessageId = ""
  53. public var referenceChatDate = ""
  54. var isHistoryCC = false
  55. var complaintId = ""
  56. var counter = 0
  57. var markerCounter: String?
  58. var buttonScrollToBottom = UIButton()
  59. let indicatorCounterBSTB = UIView()
  60. let labelCounter = UILabel()
  61. let containerActionGroup = UIView()
  62. var removed = false
  63. var isConfidential = false
  64. var isAck = false
  65. var copySession = false
  66. var forwardSession = false
  67. var deleteSession = false
  68. var isSearching = false
  69. let containerMultpileSelectSession = UIView()
  70. let viewSticker = UIView()
  71. let containerLink = UIView()
  72. let containerPreviewReply = UIView()
  73. let containerPin = UIView()
  74. let textPin = UILabel()
  75. let signSelectedPin = UIStackView()
  76. var bottomAnchorPreviewReply = NSLayoutConstraint()
  77. let containerAction = UIView()
  78. var allowTyping = true
  79. let contactChatNav = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
  80. var searchBar: UISearchBar!
  81. var constraintBottomContainerMultpileSelectSession = NSLayoutConstraint()
  82. var titleSearchMatches: UILabel!
  83. var textSearch = ""
  84. var countMatchesSearch = 0
  85. var lastScrollIdxSearch = 0
  86. var nextPinShowed = 0
  87. var buttonUp: UIButton!
  88. var buttonDown: UIButton!
  89. var keyboardHeightForMention: CGFloat?
  90. var listMentionWithText:[User] = []
  91. var listMentionInTextField:[User] = []
  92. var tempListMentionWithText:[User] = []
  93. var tempListMentionInTextField:[User] = []
  94. var showingLink = ""
  95. var isAlwaysHideLinkPreview = false
  96. var timerCheckLink: Timer?
  97. var lastPositionCursorMention = 0
  98. var lastTextLength = 0
  99. var timerFakeProgress: Timer?
  100. var showMenuContext = false
  101. var touchedSubview = UIView()
  102. var listViewOnSection: [UIView] = []
  103. var fakeProgMultip = 0
  104. let maxFakeProgMultip = 2
  105. var groupImages: [String:[ImageGrouping]] = [:]
  106. var titleText: String!
  107. var lastY: CGFloat = 0
  108. var listTimerCredential: [String: Int] = [:]
  109. var timerCredential: [String: Timer] = [:]
  110. var editVC = UIViewController()
  111. var editTextView = CustomTextView()
  112. var isEditingMessage = false
  113. var constraintBottomeditTextView: NSLayoutConstraint!
  114. var constraintHeighteditTextView: NSLayoutConstraint!
  115. var constraintBottomSendEditTV: NSLayoutConstraint!
  116. let locationManager = CLLocationManager()
  117. var longitude = ""
  118. var latitude = ""
  119. var isBlackCancelButton = false
  120. let buttonSendEdit = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
  121. var audioPlayers: [IndexPath: AVAudioPlayer] = [:]
  122. var timers: [IndexPath: Timer] = [:]
  123. var playingIndexPath: IndexPath?
  124. var timerSearch: Timer?
  125. var downloadList: [String: IndexPath] = [:]
  126. var transitioningDelegateRef: ZoomTransitioningDelegate?
  127. var buttonSpec = UIButton(type: .custom)
  128. var tableViewConfigFile: UITableView!
  129. var specFileString = ""
  130. var tableMentionEdit = UITableView()
  131. var heightTableEditMention: NSLayoutConstraint!
  132. func offset() -> CGFloat{
  133. guard let fontSize = Int(SecureUserDefaults.shared.value(forKey: "font_size") ?? "0") else { return 0 }
  134. return CGFloat(fontSize)
  135. }
  136. public override func viewDidDisappear(_ animated: Bool) {
  137. if self.isMovingFromParent {
  138. removeAllObjectBeforeDismissVC()
  139. }
  140. }
  141. private func removeAllObjectBeforeDismissVC() {
  142. for timer in self.timerCredential.values {
  143. timer.invalidate()
  144. }
  145. SecureUserDefaults.shared.removeValue(forKey: "inEditorGroup")
  146. NotificationCenter.default.removeObserver(self)
  147. self.removeFromParent()
  148. var l_pin = self.dataGroup["group_id"] as? String ?? ""
  149. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  150. l_pin = self.dataTopic["chat_id"] as? String ?? ""
  151. }
  152. var data: [String: Any] = ["text": self.textFieldSend.textColor != UIColor.lightGray ? self.textFieldSend.text! : "", "reffId": self.reffId ?? ""]
  153. if listMentionInTextField.count > 0 {
  154. var dataMention: [[String: String]] = []
  155. for list in listMentionInTextField {
  156. var dataTemp: [String: String] = [:]
  157. dataTemp["f_pin_mention"] = list.pin
  158. dataTemp["upper"] = list.ex_block
  159. dataMention.append(dataTemp)
  160. }
  161. data["list_mention"] = dataMention
  162. }
  163. if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []),
  164. let jsonString = String(data: jsonData, encoding: .utf8) {
  165. SecureUserDefaults.shared.set(jsonString, forKey: "new_saved_\(l_pin)")
  166. }
  167. }
  168. public override func viewDidAppear(_ animated: Bool) {
  169. let navBarAppearance = UINavigationBarAppearance()
  170. navBarAppearance.configureWithOpaqueBackground()
  171. navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
  172. navigationController?.navigationBar.standardAppearance = navBarAppearance
  173. navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance
  174. navigationController?.navigationBar.isTranslucent = false
  175. navigationController?.navigationBar.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
  176. navigationController?.navigationBar.tintColor = .white
  177. navigationController?.navigationBar.overrideUserInterfaceStyle = .dark
  178. self.setNeedsStatusBarAppearanceUpdate()
  179. navigationController?.navigationBar.barStyle = .black
  180. if self.navigationController?.isNavigationBarHidden ?? false {
  181. self.navigationController?.setNavigationBarHidden(false, animated: false)
  182. }
  183. navigationController?.navigationBar.prefersLargeTitles = false
  184. navigationController?.navigationItem.largeTitleDisplayMode = .never
  185. updateProfile()
  186. let indexPath = tableChatView.indexPathsForVisibleRows?.first
  187. if indexPath != nil && currentIndexpath != nil {
  188. let headerRect = tableChatView.rectForHeader(inSection: indexPath!.section)
  189. let isPinned = headerRect.origin.y <= tableChatView.contentOffset.y
  190. if listViewOnSection.count != 0 && listViewOnSection.count - 1 == indexPath!.section && isPinned {
  191. let sect = listViewOnSection.count - 1 < currentIndexpath!.section ? listViewOnSection.count - 1 : currentIndexpath!.section
  192. let headerView = listViewOnSection[sect]
  193. headerView.isHidden = true
  194. }
  195. }
  196. }
  197. public override func viewDidLoad() {
  198. super.viewDidLoad()
  199. // navigationController?.navigationBar.topItem?.title = ""
  200. Utils.addBackground(view: self.view)
  201. if let dataWall = UserDefaults.standard.data(forKey: "chatWallpaper") {
  202. wallpaperView.image = UIImage(data: UserDefaults.standard.data(forKey: "chatWallpaper")!)
  203. }
  204. else {
  205. wallpaperView.isHidden = true
  206. }
  207. if Nexilis.fromMAB {
  208. Nexilis.floatingButton.isHidden = true
  209. }
  210. viewButton.layer.shadowColor = self.traitCollection.userInterfaceStyle == .dark ? UIColor.white.cgColor : UIColor.gray.cgColor
  211. viewButton.layer.shadowOpacity = 1
  212. viewButton.layer.shadowOffset = .zero
  213. viewButton.layer.shadowRadius = 3
  214. viewButton.addTopBorder(with: UIColor.lightGray, andWidth: 1.0)
  215. // buttonVoice.setImage(resizeImage(image: UIImage(named: "Voice-Record", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)), for: .normal)
  216. viewAttachment.backgroundColor = .white
  217. buttonSendImage.setImage(resizeImage(image: UIImage(named: "Send-Image", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor), for: .normal)
  218. buttonSendPhoto.setImage(resizeImage(image: UIImage(named: "Camera", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor), for: .normal)
  219. buttonSendSticker.setImage(resizeImage(image: UIImage(named: "Sticker---Emoji", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor), for: .normal)
  220. buttonSendFile.setImage(resizeImage(image: UIImage(named: "File---Documents", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withTintColor(self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor), for: .normal)
  221. buttonSendChat.setImage(resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.blackDarkMode) : UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), for: .normal)
  222. buttonSendChat.circle()
  223. buttonSendChat.addTarget(self, action: #selector(sendTapped), for: .touchUpInside)
  224. buttonSendChat.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor
  225. buttonAckConfidential.circle()
  226. buttonAckConfidential.addTarget(self, action: #selector(showChooserACKConfidential), for: .touchUpInside)
  227. buttonAckConfidential.tintColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .white
  228. buttonAckConfidential.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor
  229. textFieldSend.backgroundColor = .white
  230. textFieldSend.layer.cornerRadius = textFieldSend.maxCornerRadius()
  231. textFieldSend.layer.borderWidth = 1.0
  232. textFieldSend.text = "Send message".localized()
  233. textFieldSend.textColor = UIColor.lightGray
  234. textFieldSend.tintColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  235. textFieldSend.textContainerInset = UIEdgeInsets(top: 12, left: 20, bottom: 11, right: 40)
  236. textFieldSend.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
  237. textFieldSend.font = UIFont.systemFont(ofSize: 12 + offset())
  238. textFieldSend.delegate = self
  239. textFieldSend.customDelegate = self
  240. textFieldSend.allowsEditingTextAttributes = true
  241. navigationItem.rightBarButtonItem?.tintColor = UIColor.secondaryColor
  242. imageVideoPicker = ImageVideoPicker(presentationController: self, delegate: self)
  243. documentPicker = DocumentPicker(presentationController: self, delegate: self)
  244. let fm = FileManager.default
  245. if Bundle.resourceBundle(for: Nexilis.self).url(forResource: "pb_gpt_bot", withExtension: "gif") != nil {
  246. let path = Bundle.resourceBundle(for: Nexilis.self).resourcePath! //resourcesMediaBundle
  247. let items = try! fm.contentsOfDirectory(atPath: path)
  248. for item in items {
  249. if item.hasPrefix("sticker") {
  250. stickers.append(item)
  251. }
  252. }
  253. } else {
  254. let path = Bundle.resourcesMediaBundle(for: Nexilis.self).resourcePath! //resourcesMediaBundle
  255. let items = try! fm.contentsOfDirectory(atPath: path)
  256. for item in items {
  257. if item.hasPrefix("sticker") {
  258. stickers.append(item)
  259. }
  260. }
  261. }
  262. tableChatView.register(UITableViewCell.self, forCellReuseIdentifier: "cellEditorGroup")
  263. loadData()
  264. setRightButtonItem()
  265. let center: NotificationCenter = NotificationCenter.default
  266. center.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  267. center.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
  268. center.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.listenerReceiveChat), object: nil)
  269. center.addObserver(self, selector: #selector(onStatusChat(notification:)), name: NSNotification.Name(rawValue: Nexilis.listenerStatusChat), object: nil)
  270. center.addObserver(self, selector: #selector(onUploadChat(notification:)), name: NSNotification.Name(rawValue: "onUploadChat"), object: nil)
  271. center.addObserver(self, selector: #selector(onMemberTopic(notification:)), name: NSNotification.Name(rawValue: "onMember"), object: nil)
  272. center.addObserver(self, selector: #selector(onGroup(notification:)), name: NSNotification.Name(rawValue: "onGroup"), object: nil)
  273. center.addObserver(self, selector: #selector(onMemberTopic(notification:)), name: NSNotification.Name(rawValue: "onTopic"), object: nil)
  274. center.addObserver(self, selector: #selector(onFailedSendMessage(notification:)), name: NSNotification.Name(rawValue: Nexilis.failedSendMessage), object: nil)
  275. center.addObserver(self, selector: #selector(onUpdatedMessage(notification:)), name: NSNotification.Name(rawValue: "onUpdatedMessage"), object: nil)
  276. center.addObserver(self, selector: #selector(onCheckNewMessages(notification:)), name: NSNotification.Name(rawValue: "checkNewMessagesNexilis"), object: nil)
  277. locationManager.delegate = self
  278. locationManager.requestWhenInUseAuthorization()
  279. DispatchQueue.global().async { [self] in
  280. if CLLocationManager.locationServicesEnabled() {
  281. locationManager.desiredAccuracy = kCLLocationAccuracyBest
  282. locationManager.startUpdatingLocation()
  283. } else {
  284. print("Location services are not enabled.")
  285. }
  286. }
  287. if dataMessageForward != nil {
  288. for i in 0..<dataMessageForward!.count {
  289. let isForwarded = (dataMessageForward![i][TypeDataMessage.is_forwarded] as? Int) ?? 0
  290. sendChat(message_scope_id: MessageScope.GROUP, status: "2", message_text: dataMessageForward![i]["message_text"] as? String ?? "", credential: "0", attachment_flag: dataMessageForward![i]["attachment_flag"] as? String ?? "", ex_blog_id: "", message_large_text: "", ex_format: "", image_id: dataMessageForward![i]["image_id"] as? String ?? "", audio_id: dataMessageForward![i]["audio_id"] as? String ?? "", video_id: dataMessageForward![i]["video_id"] as? String ?? "", file_id: dataMessageForward![i]["file_id"] as? String ?? "", thumb_id: dataMessageForward![i]["thumb_id"] as? String ?? "", reff_id: "", read_receipts: "", is_call_center: "0", call_center_id: "", viewController: self, gif_id: dataMessageForward![i][TypeDataMessage.gif_id] as? String ?? "", is_forwarded: isForwarded + 1)
  291. }
  292. dataMessageForward = nil
  293. }
  294. tableMention.register(UITableViewCell.self, forCellReuseIdentifier: "cellMention")
  295. tableMention.dataSource = self
  296. tableMention.delegate = self
  297. tableMention.contentInset = UIEdgeInsets(top: -25, left: 0, bottom: 0, right: 0)
  298. }
  299. public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
  300. guard let location = locations.last else { return }
  301. latitude = "\(location.coordinate.latitude)"
  302. longitude = "\(location.coordinate.longitude)"
  303. locationManager.stopUpdatingLocation()
  304. }
  305. public func afterUnfriend() {
  306. DispatchQueue.main.async {
  307. SecureUserDefaults.shared.removeValue(forKey: "inEditorGroup")
  308. NotificationCenter.default.removeObserver(self)
  309. }
  310. }
  311. private func updateProfile() {
  312. let idMe = User.getMyPin() as String?
  313. DispatchQueue.global().async {
  314. let message = CoreMessage_TMessageBank.getBatchBuddiesInfos(p_f_pin: idMe!, last_update: 0)
  315. let _ = Nexilis.write(message: message)
  316. }
  317. }
  318. private func setRightButtonItem() {
  319. navigationItem.rightBarButtonItems = nil
  320. navigationItem.rightBarButtonItem = nil
  321. let menu = UIMenu(title: "", children: [
  322. UIAction(title: "Delete Conversation".localized(), handler: {(_) in
  323. let alert = LibAlertController(title: "", message: "Are you sure to delete all message in this conversation?".localized(), preferredStyle: .alert)
  324. alert.addAction(UIAlertAction(title: "Cancel".localized(), style: UIAlertAction.Style.default, handler: nil))
  325. alert.addAction(UIAlertAction(title: "Delete".localized(), style: .destructive, handler: {(_) in
  326. var l_pin = self.dataGroup["group_id"] as? String ?? ""
  327. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  328. do {
  329. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  330. l_pin = self.dataTopic["chat_id"] as? String ?? ""
  331. }
  332. _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(l_pin='\(self.dataGroup["group_id"]!!)' and chat_id='\(self.dataTopic["chat_id"]!!)') and message_scope_id='4'")
  333. _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(l_pin)'")
  334. SecureUserDefaults.shared.removeValue(forKey: "new_saved_\(l_pin)")
  335. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  336. if self.fromNotification {
  337. self.didTapExit()
  338. } else {
  339. self.navigationController?.popViewController(animated: true)
  340. }
  341. } catch {
  342. rollback.pointee = true
  343. print("Access database error: \(error.localizedDescription)")
  344. }
  345. })
  346. }))
  347. self.present(alert, animated: true, completion: nil)
  348. }),
  349. ])
  350. if !isHistoryCC {
  351. let moreIcon = UIBarButtonItem(image: UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), menu: menu)
  352. let buttonSearch = UIBarButtonItem(image: UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), style: .plain, target: self, action: #selector(search(sender:)))
  353. navigationItem.rightBarButtonItems = [moreIcon,buttonSearch]
  354. } else {
  355. let buttonSearch = UIBarButtonItem(image: UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), style: .plain, target: self, action: #selector(search(sender:)))
  356. navigationItem.rightBarButtonItem = buttonSearch
  357. }
  358. }
  359. @objc func search(sender: UIBarButtonItem) {
  360. self.isSearching = true
  361. if self.reffId != nil {
  362. self.deleteReplyView()
  363. }
  364. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
  365. let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
  366. cancelButton.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], for: .normal)
  367. if !self.isHistoryCC {
  368. self.navigationItem.rightBarButtonItems = nil
  369. }
  370. self.navigationItem.rightBarButtonItem = cancelButton
  371. self.changeAppBar()
  372. self.addMultipleSelectSession()
  373. }
  374. }
  375. private func getOfficialGroup() {
  376. let query = "SELECT group_id, f_name, official, image_id FROM GROUPZ where group_type = 1 AND official = 1"
  377. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  378. do {
  379. if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
  380. if cursorData.next() {
  381. dataGroup["group_id"] = cursorData.string(forColumnIndex: 0)
  382. dataTopic["chat_id"] = ""
  383. dataGroup["f_name"] = cursorData.string(forColumnIndex: 1)
  384. dataGroup["image_id"] = cursorData.string(forColumnIndex: 3)
  385. dataGroup["official"] = cursorData.string(forColumnIndex: 2)
  386. }
  387. cursorData.close()
  388. }
  389. } catch {
  390. rollback.pointee = true
  391. print("Access database error: \(error.localizedDescription)")
  392. }
  393. })
  394. }
  395. func loadData() {
  396. if (unique_l_pin != "") {
  397. dataDates.removeAll()
  398. dataGroup.removeAll()
  399. dataTopic.removeAll()
  400. dataMessages.removeAll()
  401. tableChatView.reloadData()
  402. currentIndexpath = nil
  403. reffId = nil
  404. getDataGroup(unique_l_pin: unique_l_pin)
  405. }
  406. if removed {
  407. removed = false
  408. containerActionGroup.removeConstraints(containerActionGroup.constraints)
  409. containerActionGroup.removeFromSuperview()
  410. setRightButtonItem()
  411. }
  412. if !isHistoryCC {
  413. let groupId = dataGroup["group_id"] as? String ?? ""
  414. let chatId = dataTopic["chat_id"] as? String ?? ""
  415. let dataGT: [String] = [groupId, chatId]
  416. SecureUserDefaults.shared.set(dataGT, forKey: "inEditorGroup")
  417. if dataTopic["chat_id"] as? String ?? "" == "" {
  418. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [groupId])
  419. sendTyping(l_pin: groupId)
  420. } else {
  421. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [chatId])
  422. sendTyping(l_pin: chatId)
  423. }
  424. } else {
  425. getOfficialGroup()
  426. disableEditor()
  427. }
  428. if fromNotification {
  429. let imageButton = UIImageView(frame: CGRect(x: -16, y: 0, width: 20, height: 44))
  430. imageButton.image = UIImage(systemName: "chevron.backward", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular, scale: .default))?.withTintColor(.white)
  431. imageButton.contentMode = .left
  432. let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapExit))
  433. imageButton.isUserInteractionEnabled = true
  434. imageButton.addGestureRecognizer(tapGestureRecognizer)
  435. let leftItem = UIBarButtonItem(customView: imageButton)
  436. self.navigationItem.leftBarButtonItem = leftItem
  437. }
  438. changeAppBar()
  439. getData()
  440. getCounter()
  441. if counter > 0 && dataMessages.count >= counter {
  442. markerCounter = dataMessages[dataMessages.count - counter]["message_id"] as? String
  443. }
  444. tableChatView.alpha = 0
  445. tableChatView.delegate = self
  446. tableChatView.dataSource = self
  447. tableChatView.reloadData()
  448. if !referenceMessageId.isEmpty {
  449. if dataMessages.firstIndex(where: {$0["message_id"] as? String == referenceMessageId} ) != 0 {
  450. DispatchQueue.main.async {
  451. let section = self.dataDates.firstIndex(of: self.referenceChatDate)
  452. let row = self.dataMessages.filter({$0["chat_date"] as? String ?? "" == self.referenceChatDate}).firstIndex(where: { $0["message_id"] as? String == self.referenceMessageId})
  453. if row != nil && section != nil {
  454. let indexPath = IndexPath(row: row!, section: section!)
  455. self.tableChatView.scrollToRow(at: indexPath, at: .middle, animated: false)
  456. self.tableChatView.cellForRow(at: indexPath)?.contentView.backgroundColor = .yellow
  457. DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
  458. self.tableChatView.cellForRow(at: indexPath)?.contentView.backgroundColor = .clear
  459. })
  460. }
  461. }
  462. }
  463. } else if counter != 0 {
  464. if dataMessages.firstIndex(where: {$0["message_id"] as? String == markerCounter} ) != 0 {
  465. DispatchQueue.main.async {
  466. let data = self.dataMessages.filter({ $0["message_id"] as? String == self.markerCounter })
  467. if data.count > 0 {
  468. let section = self.dataDates.firstIndex(of: data[0]["chat_date"] as? String ?? "")
  469. let row = self.dataMessages.filter({$0["chat_date"] as? String ?? "" == data[0]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.markerCounter})
  470. self.tableChatView.scrollToRow(at: IndexPath(row: row!, section: section!), at: .bottom, animated: false)
  471. }
  472. }
  473. } else {
  474. tableChatView.scrollToTop()
  475. }
  476. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in
  477. if currentIndexpath == nil && counter != 0 {
  478. let idMe = User.getMyPin() as String?
  479. if let idx = dataMessages.firstIndex(where: { $0["message_id"] as? String == markerCounter}) {
  480. for i in idx..<dataMessages.count {
  481. if dataMessages[i]["f_pin"] as? String != idMe {
  482. sendReadMessageStatus(chat_id: self.dataTopic["chat_id"] as? String ?? "", f_pin: dataMessages[i]["f_pin"] as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: dataMessages[i]["message_id"] as? String ?? "")
  483. }
  484. }
  485. counter = 0
  486. updateCounter(counter: counter)
  487. }
  488. }
  489. }
  490. } else {
  491. var l_pin = self.dataGroup["group_id"] as? String ?? ""
  492. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  493. l_pin = self.dataTopic["chat_id"] as? String ?? ""
  494. }
  495. if let dataSaved: String = SecureUserDefaults.shared.value(forKey: "new_saved_\(l_pin)") {
  496. let data = dataSaved
  497. if let jsonData = data.data(using: .utf8),
  498. let dataJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
  499. let last_m = dataJson["text"] as? String ?? ""
  500. let last_r = dataJson["reffId"] as? String ?? ""
  501. let list_m = dataJson["list_mention"] as? [[String: String]] ?? []
  502. if !last_m.isEmpty {
  503. textFieldSend.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black
  504. }
  505. if list_m.count > 0 {
  506. for list in list_m {
  507. let f_pin = list["f_pin_mention"] ?? ""
  508. let upper = list["upper"] ?? ""
  509. let userFromBuddy = User.getData(pin: f_pin, lPin: l_pin)
  510. if userFromBuddy != nil {
  511. userFromBuddy!.ex_block = upper
  512. listMentionInTextField.append(userFromBuddy!)
  513. }
  514. }
  515. }
  516. if !last_m.isEmpty {
  517. textFieldSend.attributedText = last_m.richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "", listMentionInTextField: listMentionInTextField)
  518. }
  519. if !last_r.isEmpty {
  520. handleReply(indexPath: IndexPath(row: 0, section: 0), reffId: last_r)
  521. }
  522. }
  523. }
  524. tableChatView.scrollToBottom(isAnimated: false)
  525. }
  526. tableChatView.keyboardDismissMode = .interactive
  527. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
  528. tapGesture.cancelsTouchesInView = false
  529. tableChatView.addGestureRecognizer(tapGesture)
  530. DispatchQueue.main.asyncAfter(deadline: .now() + 0.6, execute: {
  531. if self.tableChatView.alpha != 1.0 {
  532. UIView.animate(withDuration: 0.5, animations: {
  533. self.tableChatView.alpha = 1.0
  534. })
  535. }
  536. })
  537. for data in listTimerCredential {
  538. if data.value > 0 {
  539. var second = data.value
  540. var timer = Timer()
  541. timerCredential[data.key] = timer
  542. timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in
  543. second -= 1
  544. self.listTimerCredential[data.key] = second
  545. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == data.key })
  546. if (idx != nil) {
  547. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  548. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[idx!]["message_id"] as? String })
  549. if second == 0 {
  550. timer.invalidate()
  551. self.listTimerCredential.removeValue(forKey: data.key)
  552. self.timerCredential.removeValue(forKey: data.key)
  553. SecureUserDefaults.shared.removeValue(forKey: data.key)
  554. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == data.key})
  555. if idx != nil {
  556. self.dataMessages[idx!]["lock"] = "2"
  557. self.dataMessages[idx!]["reff_id"] = ""
  558. }
  559. DispatchQueue.global().async {
  560. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  561. do {
  562. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  563. "lock" : "2"
  564. ], _where: "message_id = '\(data.key)'")
  565. } catch {
  566. rollback.pointee = true
  567. print("Access database error: \(error.localizedDescription)")
  568. }
  569. })
  570. }
  571. }
  572. if row != nil && section != nil {
  573. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  574. }
  575. }
  576. })
  577. }
  578. }
  579. let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  580. pinAllMessages(dataMessages: dataMessagesPin)
  581. }
  582. func getDataProfile(f_pin: String, message_id: String) -> [String: String]{
  583. var data: [String: String] = [:]
  584. Database.shared.database?.inTransaction({ fmdb, rollback in
  585. if let c = Database().getRecords(fmdb: fmdb, query: "select first_name || ' ' || last_name, image_id from BUDDY where f_pin = '\(f_pin)'"), c.next() {
  586. data["name"] = c.string(forColumnIndex: 0)!.trimmingCharacters(in: .whitespacesAndNewlines)
  587. data["image_id"] = c.string(forColumnIndex: 1)!
  588. c.close()
  589. }
  590. else if let c = Database().getRecords(fmdb: fmdb, query: "select first_name || ' ' || last_name, thumb_id from GROUPZ_MEMBER where f_pin = '\(f_pin)' AND group_id = '\(dataGroup["group_id"]!!)'"), c.next() {
  591. data["name"] = c.string(forColumnIndex: 0)!.trimmingCharacters(in: .whitespacesAndNewlines)
  592. data["image_id"] = c.string(forColumnIndex: 1)!
  593. c.close()
  594. } else if let c = Database().getRecords(fmdb: fmdb, query: "select f_display_name from MESSAGE where message_id = '\(message_id)'"), c.next() {
  595. data["name"] = c.string(forColumnIndex: 0)!
  596. data["image_id"] = ""
  597. c.close()
  598. } else {
  599. data["name"] = "Unknown".localized()
  600. }
  601. })
  602. return data
  603. }
  604. private func getDataGroup(unique_l_pin: String) {
  605. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  606. do {
  607. if let cursorGroup = Database.shared.getRecords(fmdb: fmdb, query: "SELECT group_id, f_name, image_id, official, parent FROM GROUPZ WHERE group_id='\(unique_l_pin)'"), cursorGroup.next() {
  608. dataGroup["group_id"] = cursorGroup.string(forColumnIndex: 0)
  609. dataGroup["f_name"] = cursorGroup.string(forColumnIndex: 1)
  610. dataGroup["image_id"] = cursorGroup.string(forColumnIndex: 2)
  611. dataGroup["official"] = cursorGroup.string(forColumnIndex: 3)
  612. dataGroup["parent"] = cursorGroup.string(forColumnIndex: 4)
  613. dataTopic["title"] = "Lounge".localized()
  614. dataTopic["chat_id"] = ""
  615. cursorGroup.close()
  616. } else if let cursorTopic = Database.shared.getRecords(fmdb: fmdb, query: "SELECT group_id, title FROM DISCUSSION_FORUM where chat_id = '\(unique_l_pin)'"), cursorTopic.next() {
  617. dataGroup["group_id"] = cursorTopic.string(forColumnIndex: 0)
  618. dataTopic["title"] = cursorTopic.string(forColumnIndex: 1)
  619. dataTopic["chat_id"] = unique_l_pin
  620. cursorTopic.close()
  621. if let cursorGroup = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_name, image_id, official, parent FROM GROUPZ where group_id = '\(dataGroup["group_id"] as? String ?? "")'"), cursorGroup.next() {
  622. dataGroup["f_name"] = cursorGroup.string(forColumnIndex: 0)
  623. dataGroup["image_id"] = cursorGroup.string(forColumnIndex: 1)
  624. dataGroup["official"] = cursorGroup.string(forColumnIndex: 2)
  625. dataGroup["parent"] = cursorGroup.string(forColumnIndex: 3)
  626. cursorGroup.close()
  627. }
  628. }
  629. } catch {
  630. rollback.pointee = true
  631. print("Access database error: \(error.localizedDescription)")
  632. }
  633. })
  634. }
  635. private func getData(offset: Int64 = 0) {
  636. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  637. do {
  638. var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"] as? String ?? "")' order by server_date asc LIMIT -1 OFFSET \(offset)"
  639. if isHistoryCC {
  640. query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where call_center_id='\(complaintId)' order by server_date asc LIMIT -1 OFFSET \(offset)"
  641. } else if (dataTopic["chat_id"] as? String ?? "" != "") {
  642. query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='\(dataTopic["chat_id"] as? String ?? "")' order by server_date asc LIMIT -1 OFFSET \(offset)"
  643. }
  644. if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
  645. var tempImages: [ImageGrouping] = []
  646. var idxOff = 0
  647. while cursorData.next() {
  648. var row: [String: Any?] = [:]
  649. row["message_id"] = cursorData.string(forColumnIndex: 0)
  650. row["f_pin"] = cursorData.string(forColumnIndex: 1)
  651. row["l_pin"] = cursorData.string(forColumnIndex: 2)
  652. row["message_scope_id"] = cursorData.string(forColumnIndex: 3)
  653. row["server_date"] = cursorData.string(forColumnIndex: 4)
  654. row["status"] = cursorData.string(forColumnIndex: 5)
  655. row["message_text"] = cursorData.string(forColumnIndex: 6)
  656. row["audio_id"] = cursorData.string(forColumnIndex: 7)
  657. row["video_id"] = cursorData.string(forColumnIndex: 8)
  658. row["image_id"] = cursorData.string(forColumnIndex: 9)
  659. row["thumb_id"] = cursorData.string(forColumnIndex: 10)
  660. row["read_receipts"] = cursorData.string(forColumnIndex: 11)
  661. row["chat_id"] = cursorData.string(forColumnIndex: 12)
  662. row["file_id"] = cursorData.string(forColumnIndex: 13)
  663. row["attachment_flag"] = cursorData.string(forColumnIndex: 14)
  664. row["reff_id"] = cursorData.string(forColumnIndex: 15)
  665. row["lock"] = cursorData.string(forColumnIndex: 16)
  666. row["is_stared"] = cursorData.string(forColumnIndex: 17)
  667. row["blog_id"] = cursorData.string(forColumnIndex: 18) ?? ""
  668. row["credential"] = cursorData.string(forColumnIndex: 19) ?? ""
  669. row[TypeDataMessage.last_edit] = cursorData.longLongInt(forColumnIndex: 20)
  670. row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 21) ?? ""
  671. row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 22))
  672. row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 23) ?? ""
  673. row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 24) ?? ""
  674. row["isSelected"] = false
  675. if row["credential"] != nil && row["credential"] as? String ?? "" == "1" {
  676. let idMe = User.getMyPin()!
  677. if row["f_pin"] as? String ?? "" == idMe {
  678. let second = getSecondsDifferenceFromTwoDates(start: Date.init(milliseconds: Int64(row["server_date"] as? String ?? "")!), end: Date())
  679. if second > 60 {
  680. listTimerCredential[row["message_id"] as? String ?? ""] = 0
  681. row["lock"] = "2"
  682. row["reff_id"] = ""
  683. DispatchQueue.global().async {
  684. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  685. do {
  686. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  687. "lock" : "2"
  688. ], _where: "message_id = '\(row["message_id"] as? String ?? "")'")
  689. } catch {
  690. rollback.pointee = true
  691. print("Access database error: \(error.localizedDescription)")
  692. }
  693. })
  694. }
  695. } else {
  696. let second = 60 - second
  697. listTimerCredential[row["message_id"] as? String ?? ""] = second
  698. }
  699. } else {
  700. let hasMessageId: String? = SecureUserDefaults.shared.value(forKey: row["message_id"] as? String ?? "") ?? nil
  701. if hasMessageId != nil {
  702. let second = getSecondsDifferenceFromTwoDates(start: Date.init(milliseconds: Int64(hasMessageId!)!), end: Date())
  703. if second > 60 {
  704. listTimerCredential[row["message_id"] as? String ?? ""] = 0
  705. row["lock"] = "2"
  706. row["reff_id"] = ""
  707. DispatchQueue.global().async {
  708. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  709. do {
  710. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  711. "lock" : "2"
  712. ], _where: "message_id = '\(row["message_id"] as? String ?? "")'")
  713. } catch {
  714. rollback.pointee = true
  715. print("Access database error: \(error.localizedDescription)")
  716. }
  717. })
  718. }
  719. } else {
  720. let second = 60 - second
  721. listTimerCredential[row["message_id"] as? String ?? ""] = second
  722. }
  723. } else {
  724. SecureUserDefaults.shared.set("\(Date().currentTimeMillis())", forKey: row["message_id"] as? String ?? "")
  725. listTimerCredential[row["message_id"] as? String ?? ""] = 60
  726. }
  727. }
  728. }
  729. row[TypeDataMessage.is_call_center] = cursorData.string(forColumnIndex: 20)
  730. row[TypeDataMessage.call_center_id] = cursorData.string(forColumnIndex: 21)
  731. row[TypeDataMessage.opposite_pin] = cursorData.string(forColumnIndex: 22)
  732. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  733. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  734. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  735. if let dirPath = paths.first {
  736. let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(row["video_id"] as? String ?? "")
  737. let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(row["file_id"] as? String ?? "")
  738. if ((row["video_id"] as? String ?? "") != "") {
  739. if FileManager.default.fileExists(atPath: videoURL.path) || FileEncryption.shared.isSecureExists(filename: row["video_id"] as? String ?? ""){
  740. row["progress"] = 100.0
  741. } else {
  742. row["progress"] = 0.0
  743. }
  744. } else {
  745. if FileManager.default.fileExists(atPath: fileURL.path) || FileEncryption.shared.isSecureExists(filename: row["file_id"] as? String ?? ""){
  746. row["progress"] = 100.0
  747. } else {
  748. row["progress"] = 0.0
  749. }
  750. }
  751. }
  752. row["chat_date"] = chatDate(stringDate: row["server_date"] as? String ?? "")
  753. if (dataMessages.count == 0 || dataMessages.last!["f_pin"] as? String ?? "" == row["f_pin"] as? String ?? "") && tempImages.count <= 30 && row["image_id"] != nil && !(row["image_id"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && (row["message_text"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && (row["reff_id"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && (row["read_receipts"] as? String ?? "") != "8" {
  754. if tempImages.count != 0 && getSecondsDifferenceFromTwoDates(start: Date.init(milliseconds: Int64(tempImages.last!.time)!), end: Date.init(milliseconds: Int64(row["server_date"] as? String ?? "")!))/60 >= 11 {
  755. if tempImages.count >= 4 {
  756. groupImages[tempImages[0].messageId] = tempImages
  757. if let idxTemp = dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == tempImages[0].messageId }) {
  758. for _ in 1..<tempImages.count {
  759. dataMessages.remove(at: idxTemp + 1)
  760. }
  761. }
  762. }
  763. tempImages.removeAll()
  764. }
  765. tempImages.append(ImageGrouping(messageId: row["message_id"] as? String ?? "", thumbId: row["thumb_id"] as? String ?? "", imageId: row["image_id"] as? String ?? "", status: row["status"] as? String ?? "", time: row["server_date"] as? String ?? "", lPin: row["l_pin"] as? String ?? "", dataMessage: row, dataPerson: [:], dataGroup: dataGroup, dataTopic: dataTopic))
  766. } else if tempImages.count >= 4 {
  767. groupImages[tempImages[0].messageId] = tempImages
  768. if let idxTemp = dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == tempImages[0].messageId }) {
  769. for _ in 1..<tempImages.count {
  770. dataMessages.remove(at: idxTemp + 1)
  771. }
  772. }
  773. tempImages.removeAll()
  774. } else if tempImages.count != 0 {
  775. tempImages.removeAll()
  776. }
  777. if offset > 0 && idxOff == 0 {
  778. self.markerCounter = row["message_id"] as? String
  779. }
  780. dataMessages.append(row)
  781. idxOff+=1
  782. }
  783. // if isHistoryCC {
  784. // dataMessages.remove(at: 0)
  785. // }
  786. if tempImages.count >= 4 {
  787. if tempImages.count > 30 {
  788. tempImages.removeSubrange(30..<tempImages.count)
  789. }
  790. groupImages[tempImages[0].messageId] = tempImages
  791. if let idxTemp = dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == tempImages[0].messageId }) {
  792. for _ in 1..<tempImages.count {
  793. dataMessages.remove(at: idxTemp + 1)
  794. }
  795. }
  796. }
  797. cursorData.close()
  798. }
  799. } catch {
  800. rollback.pointee = true
  801. print("Access database error: \(error.localizedDescription)")
  802. }
  803. })
  804. }
  805. func getSecondsDifferenceFromTwoDates(start: Date, end: Date) -> Int {
  806. let diff = Int(end.timeIntervalSince1970 - start.timeIntervalSince1970)
  807. let hours = diff / 3600
  808. let seconds = (diff - hours * 3600)
  809. return seconds
  810. }
  811. private func getRealStatus(messageId: String) -> String {
  812. var status = "-1"
  813. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  814. do {
  815. if let cursorStatus = Database.shared.getRecords(fmdb: fmdb, query: "SELECT status, f_pin FROM MESSAGE_STATUS WHERE message_id='\(messageId)'") {
  816. var listStatus: [Int] = []
  817. while cursorStatus.next() {
  818. listStatus.append(Int(cursorStatus.string(forColumnIndex: 0)!)!)
  819. }
  820. cursorStatus.close()
  821. status = "\(listStatus.min() ?? -1)"
  822. }
  823. } catch {
  824. rollback.pointee = true
  825. print("Access database error: \(error.localizedDescription)")
  826. }
  827. })
  828. return status
  829. }
  830. private func chatDate(stringDate: String) -> String {
  831. let date = Date(milliseconds: Int64(stringDate)!)
  832. let calendar = Calendar.current
  833. if (calendar.isDateInToday(date)) {
  834. if !dataDates.contains("Today".localized()){
  835. dataDates.append("Today".localized())
  836. }
  837. return "Today".localized()
  838. } else {
  839. let startOfNow = calendar.startOfDay(for: Date())
  840. let startOfTimeStamp = calendar.startOfDay(for: date)
  841. let components = calendar.dateComponents([.day], from: startOfNow, to: startOfTimeStamp)
  842. let day = -(components.day!)
  843. if day == 1{
  844. if !dataDates.contains("Yesterday".localized()){
  845. dataDates.append("Yesterday".localized())
  846. }
  847. return "Yesterday".localized()
  848. } else if day < 7 {
  849. let formatter = DateFormatter()
  850. formatter.dateFormat = "EEEE"
  851. let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
  852. if lang == "id" {
  853. formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
  854. }
  855. if !dataDates.contains(formatter.string(from: date)){
  856. dataDates.append(formatter.string(from: date))
  857. }
  858. return formatter.string(from: date)
  859. } else {
  860. let formatter = DateFormatter()
  861. formatter.dateFormat = "EE, dd MMM"
  862. let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
  863. if lang == "id" {
  864. formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
  865. }
  866. let stringFormat = formatter.string(from: date as Date)
  867. if !dataDates.contains(stringFormat){
  868. dataDates.append(stringFormat)
  869. }
  870. return stringFormat
  871. }
  872. }
  873. }
  874. private func changeAppBar() {
  875. let viewAppBar = UIView()
  876. viewAppBar.frame.size = CGSize(width: self.view.frame.size.width, height: 44)
  877. if !isSearching {
  878. let imageProfile = UIImageView(frame: CGRect(x: 0, y: 7, width: 30, height: 30))
  879. imageProfile.circle()
  880. imageProfile.clipsToBounds = true
  881. viewAppBar.addSubview(imageProfile)
  882. let pictureImage = dataGroup["image_id"] ?? ""
  883. if (pictureImage as? String ?? "" != "" && pictureImage != nil) {
  884. imageProfile.setImage(name: pictureImage! as? String ?? "")
  885. imageProfile.contentMode = .scaleAspectFill
  886. } else {
  887. imageProfile.image = UIImage(systemName: "person.3")
  888. imageProfile.contentMode = .scaleAspectFit
  889. imageProfile.backgroundColor = .lightGray
  890. }
  891. var widthTitle = viewAppBar.frame.size.width - 180
  892. if isHistoryCC {
  893. widthTitle = viewAppBar.frame.size.width - 150
  894. }
  895. let titleNavigation = UILabel(frame: CGRect(x: 35, y: 0, width: widthTitle, height: 44))
  896. viewAppBar.addSubview(titleNavigation)
  897. if (dataGroup["official"] as? String ?? "" == "1") {
  898. if !isHistoryCC {
  899. titleNavigation.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: " \(dataGroup["f_name"]!!) (\(dataTopic["title"]!!))", size: 15, y: -4)
  900. } else {
  901. titleNavigation.text = (dataGroup["f_name"] as? String)! + " " + "Contact Center".localized()
  902. }
  903. } else {
  904. titleNavigation.text = (dataGroup["f_name"] as? String ?? "") + " (\(dataTopic["title"] as? String ?? ""))"
  905. }
  906. titleNavigation.textColor = .white
  907. titleNavigation.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  908. navigationItem.titleView = viewAppBar
  909. titleText = titleNavigation.text
  910. } else {
  911. searchBar = UISearchBar()
  912. searchBar.autocapitalizationType = .none
  913. searchBar.delegate = self
  914. searchBar.searchTextField.tintColor = .mainColor
  915. searchBar.searchTextField.textColor = .mainColor
  916. // searchBar.updateHeight(height: 36, radius: 18)
  917. searchBar.showsCancelButton = false
  918. // searchBar.setMagnifyingGlassColorTo(color: .white)
  919. searchBar.setImage(UIImage(), for: .search, state: .normal)
  920. searchBar.setPositionAdjustment(UIOffset(horizontal: 10, vertical: 0), for: .search)
  921. searchBar.setCustomBackgroundImage(image: UIImage(named: self.traitCollection.userInterfaceStyle == .dark ? "nx_search_bar_dark" : "nx_search_bar", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!)
  922. navigationItem.titleView = searchBar
  923. self.definesPresentationContext = true
  924. }
  925. if copySession || forwardSession || deleteSession || isSearching {
  926. navigationItem.hidesBackButton = true
  927. navigationController?.interactivePopGestureRecognizer?.isEnabled = false
  928. } else {
  929. navigationItem.hidesBackButton = false
  930. navigationController?.interactivePopGestureRecognizer?.isEnabled = true
  931. }
  932. viewAppBar.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(seeProfileTapped)))
  933. }
  934. func updateProgress(_ data: [AnyHashable: Any]){
  935. var isImage = false
  936. var idx = dataMessages.lastIndex(where: { $0["video_id"] as? String ?? "" == data["name"] as? String ?? "" || $0["video_id"] as? String == data["video_id"] as? String })
  937. if (idx == nil) {
  938. idx = dataMessages.lastIndex(where: { $0["image_id"] as? String ?? "" == data["name"] as? String ?? "" || $0["image_id"] as? String == data["image_id"] as? String })
  939. isImage = true
  940. }
  941. if (idx != nil) {
  942. let section = dataDates.firstIndex(of: dataMessages[idx!]["chat_date"] as? String ?? "")
  943. if section == nil {
  944. return
  945. }
  946. let row = dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String ?? "" == dataMessages[idx!]["message_id"] as? String ?? ""})
  947. if row == nil {
  948. return
  949. }
  950. DispatchQueue.main.async {
  951. let indexPath = IndexPath(row: row!, section: section!)
  952. if(self.fakeProgMultip < self.maxFakeProgMultip){
  953. self.fakeProgMultip = self.fakeProgMultip + 1
  954. }
  955. let fakeProgress = Double(self.fakeProgMultip) * (100.0 / Double(self.maxFakeProgMultip))
  956. let progress = max(data["progress"] as! Double, fakeProgress)
  957. if(data["progress"] as! Double == 100.0){
  958. self.fakeProgMultip = 0
  959. }
  960. if let cell = self.tableChatView.cellForRow(at: indexPath) {
  961. for view in cell.contentView.subviews {
  962. if !(view is UITextView) && !(view is UIImageView) {
  963. for viewInContainer in view.subviews {
  964. if viewInContainer is UIImageView {
  965. if viewInContainer.subviews.count == 0 {
  966. return
  967. }
  968. var containerView : UIView?
  969. if (isImage) {
  970. containerView = viewInContainer.subviews[0]
  971. } else if viewInContainer.subviews.count > 1 {
  972. containerView = viewInContainer.subviews[1]
  973. }
  974. if let loading = containerView?.layer.sublayers?[1] as? CAShapeLayer {
  975. loading.strokeEnd = CGFloat(progress / 100)
  976. if (progress == 100.0) {
  977. self.dataMessages[idx!]["progress"] = progress
  978. self.tableChatView.reloadRows(at: [indexPath], with: .none)
  979. }
  980. }
  981. }
  982. }
  983. }
  984. }
  985. }
  986. }
  987. } else {
  988. idx = dataMessages.lastIndex(where: { $0["file_id"] as? String ?? "" == data["name"] as? String ?? "" || $0["file_id"] as? String == data["file_id"] as? String })
  989. if (idx != nil) {
  990. DispatchQueue.main.async {
  991. let section = 0
  992. let indexPath = IndexPath(row: idx!, section: section)
  993. if(self.fakeProgMultip < self.maxFakeProgMultip){
  994. self.fakeProgMultip = self.fakeProgMultip + 1
  995. }
  996. let fakeProgress = Double(self.fakeProgMultip) * (100.0 / Double(self.maxFakeProgMultip))
  997. let progress = max(data["progress"] as! Double, fakeProgress)
  998. if(data["progress"] as! Double == 100.0){
  999. self.fakeProgMultip = 0
  1000. }
  1001. if let cell = self.tableChatView.cellForRow(at: indexPath) {
  1002. for view in cell.contentView.subviews {
  1003. if !(view is UITextView) && !(view is UIImageView) {
  1004. for viewSubviews in view.subviews {
  1005. if !(viewSubviews is UITextView) {
  1006. for viewInContainer in viewSubviews.subviews {
  1007. if !(viewInContainer is UITextView) && !(viewInContainer is UIImageView) {
  1008. if let cont = viewInContainer.layer.sublayers {
  1009. if cont.count < 2 {
  1010. return
  1011. }
  1012. }
  1013. if let layers = viewInContainer.layer.sublayers {
  1014. if let loading = layers [1] as? CAShapeLayer {
  1015. loading.strokeEnd = CGFloat(progress / 100)
  1016. if (progress == 100.0) {
  1017. self.dataMessages[idx!]["progress"] = progress
  1018. self.tableChatView.reloadRows(at: [indexPath], with: .none)
  1019. }
  1020. }
  1021. }
  1022. }
  1023. }
  1024. }
  1025. }
  1026. }
  1027. }
  1028. }
  1029. }
  1030. }
  1031. }
  1032. }
  1033. @objc func onUploadChat(notification: NSNotification) {
  1034. let data:[AnyHashable : Any] = notification.userInfo!
  1035. updateProgress(data)
  1036. }
  1037. @objc func onCheckNewMessages(notification: NSNotification) {
  1038. var query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='' AND l_pin='\(dataGroup["group_id"] as? String ?? "")' order by server_date asc"
  1039. if isHistoryCC {
  1040. query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared FROM MESSAGE where call_center_id='\(complaintId)' order by server_date asc"
  1041. } else if (dataTopic["chat_id"] as? String ?? "" != "") {
  1042. query = "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned FROM MESSAGE where chat_id='\(dataTopic["chat_id"] as? String ?? "")' order by server_date asc"
  1043. }
  1044. var countMessagesNow: Int64 = 0
  1045. DispatchQueue.main.async { [self] in
  1046. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  1047. do {
  1048. if let cursorCount = Database.shared.getRecords(fmdb: fmdb, query: query), cursorCount.next() {
  1049. countMessagesNow = Int64(cursorCount.int(forColumnIndex: 0))
  1050. cursorCount.close()
  1051. }
  1052. }catch{}
  1053. })
  1054. if dataMessages.count < countMessagesNow {
  1055. self.counter = Int(countMessagesNow) - dataMessages.count
  1056. getData(offset: Int64(self.dataMessages.count))
  1057. tableChatView.reloadData()
  1058. if !self.indicatorCounterBSTB.isDescendant(of: self.view) && !self.buttonScrollToBottom.isDescendant(of: self.view) {
  1059. let indexMessage = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.markerCounter })
  1060. if indexMessage != nil {
  1061. let section = self.dataDates.firstIndex(of: self.dataMessages[indexMessage!]["chat_date"] as? String ?? "")
  1062. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[indexMessage!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[indexMessage!]["message_id"] as? String })
  1063. self.tableChatView.scrollToRow(at: IndexPath(row: row!, section: section!), at: .top, animated: true)
  1064. }
  1065. } else if self.buttonScrollToBottom.isDescendant(of: self.view) {
  1066. if !self.indicatorCounterBSTB.isDescendant(of: self.view) {
  1067. addCounterAtButttonScrollToBottom()
  1068. } else {
  1069. self.labelCounter.text = "\(counter)"
  1070. }
  1071. } else {
  1072. addButtonScrollToBottom()
  1073. addCounterAtButttonScrollToBottom()
  1074. }
  1075. }
  1076. }
  1077. }
  1078. @objc func onUpdatedMessage(notification: NSNotification) {
  1079. DispatchQueue.main.async {
  1080. let data:[AnyHashable : Any] = notification.userInfo!
  1081. let messageId = data["message_id"] as? String ?? ""
  1082. let messageIdNotif = data["message_id_notif"] as? String ?? ""
  1083. let isPinned = data["is_pinned"] as? String ?? ""
  1084. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == messageId})
  1085. if idx != nil{
  1086. self.dataMessages[idx!][TypeDataMessage.is_pinned] = isPinned
  1087. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  1088. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String ?? "" == self.dataMessages[idx!]["message_id"] as? String ?? "" })
  1089. if row != nil && section != nil {
  1090. DispatchQueue.main.async {
  1091. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1092. }
  1093. }
  1094. let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  1095. self.pinAllMessages(dataMessages: dataMessagesPin)
  1096. if !messageIdNotif.isEmpty {
  1097. self.appendNewMessage(messageId: messageIdNotif)
  1098. }
  1099. }
  1100. }
  1101. }
  1102. @objc func onReceiveMessage(notification: NSNotification) {
  1103. DispatchQueue.main.async {
  1104. let data:[AnyHashable : Any] = notification.userInfo!
  1105. if let dataMessage = data["message"] as? TMessage {
  1106. let chatData = dataMessage.mBodies
  1107. let group_id = self.dataGroup["group_id"] as? String ?? ""
  1108. let chat_id = self.dataTopic["chat_id"] as? String ?? ""
  1109. if chatData[CoreMessage_TMessageKey.L_PIN] == group_id && (chatData[CoreMessage_TMessageKey.CHAT_ID] ?? "") == chat_id {
  1110. let idx = self.dataMessages.firstIndex(where: { $0[TypeDataMessage.message_id] as? String == chatData[CoreMessage_TMessageKey.MESSAGE_ID]})
  1111. if idx != nil {
  1112. self.dataMessages[idx!][TypeDataMessage.message_text] = chatData[CoreMessage_TMessageKey.MESSAGE_TEXT]
  1113. self.dataMessages[idx!][TypeDataMessage.last_edit] = Int64(chatData[CoreMessage_TMessageKey.LAST_EDIT]!)
  1114. self.dataMessages[idx!][TypeDataMessage.status] = chatData[CoreMessage_TMessageKey.STATUS]
  1115. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  1116. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[idx!]["message_id"] as? String })
  1117. if row != nil && section != nil {
  1118. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1119. }
  1120. return
  1121. }
  1122. var row: [String: Any?] = [:]
  1123. row["message_id"] = chatData[CoreMessage_TMessageKey.MESSAGE_ID]
  1124. row["f_pin"] = chatData[CoreMessage_TMessageKey.F_PIN]
  1125. row["l_pin"] = chatData[CoreMessage_TMessageKey.L_PIN]
  1126. row["message_scope_id"] = chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID]
  1127. row["server_date"] = chatData[CoreMessage_TMessageKey.SERVER_DATE]
  1128. row["status"] = chatData[CoreMessage_TMessageKey.STATUS]
  1129. row["message_text"] = chatData[CoreMessage_TMessageKey.MESSAGE_TEXT]
  1130. if (chatData.keys.contains(CoreMessage_TMessageKey.AUDIO_ID)) {
  1131. row["audio_id"] = chatData[CoreMessage_TMessageKey.AUDIO_ID]
  1132. } else {
  1133. row["audio_id"] = ""
  1134. }
  1135. if (chatData.keys.contains(CoreMessage_TMessageKey.GIF_ID)) {
  1136. row["gif_id"] = chatData[CoreMessage_TMessageKey.GIF_ID]
  1137. } else {
  1138. row["gif_id"] = ""
  1139. }
  1140. if (chatData.keys.contains(CoreMessage_TMessageKey.VIDEO_ID)) {
  1141. row["video_id"] = chatData[CoreMessage_TMessageKey.VIDEO_ID]
  1142. } else {
  1143. row["video_id"] = ""
  1144. }
  1145. if (chatData.keys.contains(CoreMessage_TMessageKey.IMAGE_ID)) {
  1146. row["image_id"] = chatData[CoreMessage_TMessageKey.IMAGE_ID]
  1147. } else {
  1148. row["image_id"] = ""
  1149. }
  1150. if (chatData.keys.contains(CoreMessage_TMessageKey.THUMB_ID)) {
  1151. row["thumb_id"] = chatData[CoreMessage_TMessageKey.THUMB_ID]
  1152. } else {
  1153. row["thumb_id"] = ""
  1154. }
  1155. if (chatData.keys.contains(CoreMessage_TMessageKey.CHAT_ID)) {
  1156. row["chat_id"] = chatData[CoreMessage_TMessageKey.CHAT_ID]
  1157. } else {
  1158. row["chat_id"] = ""
  1159. }
  1160. if (chatData.keys.contains(CoreMessage_TMessageKey.FILE_ID)) {
  1161. row["file_id"] = chatData[CoreMessage_TMessageKey.FILE_ID]
  1162. } else {
  1163. row["file_id"] = ""
  1164. }
  1165. if (chatData.keys.contains(CoreMessage_TMessageKey.READ_RECEIPTS)) {
  1166. row["read_receipts"] = chatData[CoreMessage_TMessageKey.READ_RECEIPTS]
  1167. } else {
  1168. row["read_receipts"] = ""
  1169. }
  1170. if (chatData.keys.contains(CoreMessage_TMessageKey.CREDENTIAL)) {
  1171. row["credential"] = chatData[CoreMessage_TMessageKey.CREDENTIAL]
  1172. } else {
  1173. row["credential"] = ""
  1174. }
  1175. row["progress"] = 0.0
  1176. row["attachment_flag"] = chatData[CoreMessage_TMessageKey.ATTACHMENT_FLAG]
  1177. row["reff_id"] = chatData[CoreMessage_TMessageKey.REF_ID] ?? ""
  1178. row["lock"] = ""
  1179. row["is_stared"] = "0"
  1180. row[TypeDataMessage.is_forwarded] = Int(chatData[CoreMessage_TMessageKey.IS_FORWARDED_MESSAGE] ?? "0")
  1181. row[TypeDataMessage.spec_file] = chatData[CoreMessage_TMessageKey.ATTACHMENT_SPECIALITY]
  1182. row["isSelected"] = false
  1183. if !self.dataDates.contains("Today".localized()){
  1184. self.dataDates.append("Today".localized())
  1185. self.tableChatView.insertSections(IndexSet(integer: self.dataDates.count - 1), with: .fade)
  1186. }
  1187. row["chat_date"] = "Today".localized()
  1188. row["blog_id"] = chatData[CoreMessage_TMessageKey.BLOG_ID]
  1189. if row["credential"] != nil && row["credential"] as? String ?? "" == "1" {
  1190. self.listTimerCredential[row["message_id"] as? String ?? ""] = 60
  1191. }
  1192. self.counter += 1
  1193. self.tableChatView.beginUpdates()
  1194. self.dataMessages.append(row)
  1195. self.tableChatView.insertRows(at: [IndexPath(row: self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1, section: self.dataDates.count - 1)], with: .fade)
  1196. self.tableChatView.endUpdates()
  1197. if row["credential"] != nil && row["credential"] as? String ?? "" == "1" {
  1198. var timer = Timer()
  1199. var minute = 60
  1200. self.timerCredential[row["message_id"] as? String ?? ""] = timer
  1201. SecureUserDefaults.shared.set("\(Date().currentTimeMillis())", forKey: row["message_id"] as? String ?? "")
  1202. timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in
  1203. minute -= 1
  1204. self.listTimerCredential[row["message_id"] as? String ?? ""] = minute
  1205. if minute == 0 {
  1206. timer.invalidate()
  1207. self.listTimerCredential.removeValue(forKey: row["message_id"] as? String ?? "")
  1208. self.timerCredential.removeValue(forKey: row["message_id"] as? String ?? "")
  1209. SecureUserDefaults.shared.removeValue(forKey: row["message_id"] as? String ?? "")
  1210. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == row["message_id"] as? String})
  1211. if idx != nil {
  1212. self.dataMessages[idx!]["lock"] = "2"
  1213. self.dataMessages[idx!]["reff_id"] = ""
  1214. }
  1215. DispatchQueue.global().async {
  1216. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  1217. do {
  1218. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  1219. "lock" : "2"
  1220. ], _where: "message_id = '\(row["message_id"] as? String ?? "")'")
  1221. } catch {
  1222. rollback.pointee = true
  1223. print("Access database error: \(error.localizedDescription)")
  1224. }
  1225. })
  1226. }
  1227. }
  1228. let section = self.dataDates.firstIndex(of: self.dataDates[self.dataDates.count - 1])
  1229. let row = self.dataMessages.filter({$0["chat_date"] as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).firstIndex(where: { $0["message_id"] as? String == row["message_id"] as? String})
  1230. let indexPath = IndexPath(row: row!, section: section!)
  1231. if row != nil && section != nil{
  1232. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1233. }
  1234. })
  1235. }
  1236. if self.currentIndexpath?.row == (self.dataMessages.count - 2) {
  1237. if (self.viewIfLoaded?.window != nil) {
  1238. self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"] as? String ?? "", f_pin: chatData[CoreMessage_TMessageKey.F_PIN]!, message_scope_id: chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID]!, message_id: chatData[CoreMessage_TMessageKey.MESSAGE_ID]!)
  1239. }
  1240. self.tableChatView.scrollToBottom()
  1241. if (self.currentIndexpath!.section <= self.dataDates.count - 1 && self.currentIndexpath!.row <= self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1) {
  1242. self.counter = 0
  1243. self.updateCounter(counter: self.counter)
  1244. }
  1245. let lastMarkerCounter = self.markerCounter
  1246. if self.markerCounter != nil {
  1247. self.markerCounter = nil
  1248. }
  1249. let indexMessage = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == lastMarkerCounter })
  1250. if indexMessage != nil {
  1251. let section = self.dataDates.firstIndex(of: self.dataMessages[indexMessage!]["chat_date"] as? String ?? "")
  1252. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[indexMessage!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[indexMessage!]["message_id"] as? String })
  1253. if row != nil && section != nil {
  1254. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1255. }
  1256. }
  1257. }
  1258. else if self.currentIndexpath == nil {
  1259. self.counter = 0
  1260. self.updateCounter(counter: self.counter)
  1261. if (self.viewIfLoaded?.window != nil) {
  1262. self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"] as? String ?? "", f_pin: chatData[CoreMessage_TMessageKey.F_PIN]!, message_scope_id: chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID]!, message_id: chatData[CoreMessage_TMessageKey.MESSAGE_ID]!)
  1263. }
  1264. }
  1265. else if self.counter != 0 {
  1266. if !self.indicatorCounterBSTB.isDescendant(of: self.view) && self.buttonScrollToBottom.isDescendant(of: self.view) {
  1267. self.markerCounter = row["message_id"] as? String
  1268. self.addCounterAtButttonScrollToBottom()
  1269. let indexMessage = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.markerCounter })
  1270. if indexMessage != nil {
  1271. let section = self.dataDates.firstIndex(of: self.dataMessages[indexMessage!]["chat_date"] as? String ?? "")
  1272. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[indexMessage!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[indexMessage!]["message_id"] as? String })
  1273. if row != nil && section != nil {
  1274. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1275. }
  1276. }
  1277. } else if self.indicatorCounterBSTB.isDescendant(of: self.view) {
  1278. self.labelCounter.text = "\(self.counter)"
  1279. }
  1280. }
  1281. } else {
  1282. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  1283. }
  1284. } else {
  1285. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  1286. }
  1287. }
  1288. }
  1289. @objc func onStatusChat(notification: NSNotification) {
  1290. DispatchQueue.main.async {
  1291. let data:[AnyHashable : Any] = notification.userInfo!
  1292. if let dataMessage = data["message"] as? TMessage {
  1293. let idMe = User.getMyPin() as String?
  1294. let chatData = dataMessage.mBodies
  1295. if (chatData[CoreMessage_TMessageKey.F_PIN] == idMe || chatData[CoreMessage_TMessageKey.L_PIN] == self.dataGroup["group_id"] as? String || chatData[CoreMessage_TMessageKey.F_PIN] == self.dataGroup["group_id"] as? String) && chatData[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] == MessageScope.GROUP {
  1296. if (chatData.keys.contains(CoreMessage_TMessageKey.MESSAGE_ID) && !(chatData[CoreMessage_TMessageKey.MESSAGE_ID]!).contains("-2,")) {
  1297. var idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == chatData[CoreMessage_TMessageKey.MESSAGE_ID]! })
  1298. if let idxMessageIdParent = self.groupImages.firstIndex(where: { $0.value.contains(where: { $0.messageId == chatData[CoreMessage_TMessageKey.MESSAGE_ID]! }) }) {
  1299. if let idxInImages = self.groupImages[idxMessageIdParent].value.firstIndex(where: { $0.messageId == chatData[CoreMessage_TMessageKey.MESSAGE_ID]! }) {
  1300. self.groupImages[idxMessageIdParent].value[idxInImages].status = chatData[CoreMessage_TMessageKey.STATUS]!
  1301. self.groupImages[idxMessageIdParent].value[idxInImages].dataMessage["status"] = chatData[CoreMessage_TMessageKey.STATUS]!
  1302. }
  1303. idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.groupImages[idxMessageIdParent].key })
  1304. }
  1305. if (idx != nil) {
  1306. if (chatData[CoreMessage_TMessageKey.DELETE_MESSAGE_FLAG] == "1") {
  1307. self.updateStatusDelete(idx: idx, chatData: chatData)
  1308. } else {
  1309. self.updateStatusMessage(idx: idx, chatData: chatData)
  1310. }
  1311. }
  1312. }
  1313. else if (chatData.keys.contains("message_id")) {
  1314. var idMessage = dataMessage.getBody(key: "message_id")
  1315. if idMessage.contains("'") {
  1316. idMessage = idMessage.replacingOccurrences(of: "'", with: "")
  1317. }
  1318. var idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == idMessage })
  1319. if let idxMessageIdParent = self.groupImages.firstIndex(where: { $0.value.contains(where: { $0.messageId == idMessage }) }) {
  1320. if let idxInImages = self.groupImages[idxMessageIdParent].value.firstIndex(where: { $0.messageId == idMessage }) {
  1321. self.groupImages[idxMessageIdParent].value[idxInImages].status = chatData[CoreMessage_TMessageKey.STATUS]!
  1322. self.groupImages[idxMessageIdParent].value[idxInImages].dataMessage["status"] = chatData[CoreMessage_TMessageKey.STATUS]!
  1323. }
  1324. idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.groupImages[idxMessageIdParent].key })
  1325. }
  1326. if (idx != nil) {
  1327. if (chatData[CoreMessage_TMessageKey.DELETE_MESSAGE_FLAG] == "1") {
  1328. self.updateStatusDelete(idx: idx, chatData: chatData)
  1329. } else {
  1330. self.updateStatusMessage(idx: idx, chatData: chatData)
  1331. }
  1332. }
  1333. }
  1334. else {
  1335. let listMessageId = chatData[CoreMessage_TMessageKey.MESSAGE_ID]!.split(separator: ",")
  1336. for i in 1..<listMessageId.count {
  1337. var idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == listMessageId[i] })
  1338. if let idxMessageIdParent = self.groupImages.firstIndex(where: { $0.value.contains(where: { $0.messageId == listMessageId[i] }) }) {
  1339. if let idxInImages = self.groupImages[idxMessageIdParent].value.firstIndex(where: { $0.messageId == listMessageId[i] }) {
  1340. self.groupImages[idxMessageIdParent].value[idxInImages].status = chatData[CoreMessage_TMessageKey.STATUS]!
  1341. self.groupImages[idxMessageIdParent].value[idxInImages].dataMessage["status"] = chatData[CoreMessage_TMessageKey.STATUS]!
  1342. }
  1343. idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.groupImages[idxMessageIdParent].key })
  1344. }
  1345. if (idx != nil) {
  1346. self.updateStatusMessage(idx: idx, chatData: chatData)
  1347. }
  1348. }
  1349. }
  1350. }
  1351. }
  1352. }
  1353. }
  1354. @objc func onFailedSendMessage(notification: NSNotification) {
  1355. DispatchQueue.main.async {
  1356. let data:[AnyHashable : Any] = notification.userInfo!
  1357. let messageId = data["message_id"] as? String ?? ""
  1358. let status = data["status"] as? String ?? ""
  1359. var idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == messageId })
  1360. if let idxMessageIdParent = self.groupImages.firstIndex(where: { $0.value.contains(where: { $0.messageId == messageId }) }) {
  1361. if let idxInImages = self.groupImages[idxMessageIdParent].value.firstIndex(where: { $0.messageId == messageId }) {
  1362. self.groupImages[idxMessageIdParent].value[idxInImages].status = status
  1363. self.groupImages[idxMessageIdParent].value[idxInImages].dataMessage["status"] = status
  1364. }
  1365. idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.groupImages[idxMessageIdParent].key })
  1366. }
  1367. if (idx != nil) {
  1368. do {
  1369. self.dataMessages[idx!]["status"] = status
  1370. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  1371. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[idx!]["message_id"] as? String })
  1372. if row != nil && section != nil {
  1373. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1374. }
  1375. } catch {
  1376. }
  1377. }
  1378. }
  1379. }
  1380. private func updateStatusDelete(idx: Int?, chatData: [String: String]) {
  1381. do {
  1382. if self.dataMessages[idx!]["lock"] != nil && self.dataMessages[idx!]["lock"] as? String ?? "" == "1" {
  1383. return
  1384. }
  1385. self.dataMessages[idx!]["lock"] = "1"
  1386. self.dataMessages[idx!]["reff_id"] = ""
  1387. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  1388. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String ?? "" == self.dataMessages[idx!]["message_id"] as? String ?? "" })
  1389. if row != nil && section != nil {
  1390. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1391. }
  1392. if self.listTimerCredential[self.dataMessages[idx!]["message_id"] as? String ?? ""] != nil {
  1393. self.listTimerCredential.removeValue(forKey: self.dataMessages[idx!]["message_id"] as? String ?? "")
  1394. self.timerCredential[self.dataMessages[idx!]["message_id"] as? String ?? ""]?.invalidate()
  1395. self.timerCredential.removeValue(forKey: self.dataMessages[idx!]["message_id"] as? String ?? "")
  1396. SecureUserDefaults.shared.removeValue(forKey: self.dataMessages[idx!]["message_id"] as? String ?? "")
  1397. }
  1398. if self.reffId != nil && self.reffId == chatData["message_id"]! {
  1399. self.deleteReplyView()
  1400. }
  1401. } catch {
  1402. }
  1403. }
  1404. private func updateStatusMessage(idx: Int?, chatData: [String: String]) {
  1405. do {
  1406. if Int(self.dataMessages[idx!]["status"] as? String ?? "")! > Int(chatData[CoreMessage_TMessageKey.STATUS]!)! {
  1407. return
  1408. }
  1409. self.dataMessages[idx!]["status"] = chatData[CoreMessage_TMessageKey.STATUS]!
  1410. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  1411. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String ?? "" == self.dataMessages[idx!]["message_id"] as? String ?? "" })
  1412. if row != nil && section != nil {
  1413. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  1414. }
  1415. } catch {
  1416. }
  1417. }
  1418. @objc func onMemberTopic(notification: NSNotification) {
  1419. let data:[AnyHashable : Any] = notification.userInfo!
  1420. DispatchQueue.main.async { [self] in
  1421. if data["member"] == nil || data["code"] as? String ?? "" == CoreMessage_TMessageCode.EXIT_GROUP && data["member"] as? String ?? "" == User.getMyPin()! && data["groupId"] as? String ?? "" == self.dataGroup["group_id"] as? String ?? "" && !containerActionGroup.isDescendant(of: self.view) {
  1422. dismissKeyboard()
  1423. let labelKicked = UILabel()
  1424. if data["member"] == nil && data["code"] as? String ?? "" == CoreMessage_TMessageCode.DELETE_CHAT && data["topicId"] as? String ?? "" == dataTopic["chat_id"] as? String ?? "" {
  1425. labelKicked.text = "This topic has been deleted".localized()
  1426. } else if data["member"] != nil && data["member"] as? String ?? "" == data["f_pin"] as? String ?? "" {
  1427. labelKicked.text = "You have left this group".localized()
  1428. } else if data["member"] != nil {
  1429. labelKicked.text = "You have been removed from this group".localized()
  1430. } else if data["code"] as? String ?? "" == CoreMessage_TMessageCode.UPDATE_CHAT {
  1431. dataGroup.removeAll()
  1432. dataTopic.removeAll()
  1433. getDataGroup(unique_l_pin: unique_l_pin)
  1434. changeAppBar()
  1435. return
  1436. } else {
  1437. return
  1438. }
  1439. removed = true
  1440. cancelAction()
  1441. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: { [self] in
  1442. navigationItem.rightBarButtonItem = nil
  1443. view.addSubview(containerActionGroup)
  1444. containerActionGroup.translatesAutoresizingMaskIntoConstraints = false
  1445. NSLayoutConstraint.activate([
  1446. containerActionGroup.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
  1447. containerActionGroup.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
  1448. containerActionGroup.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
  1449. containerActionGroup.heightAnchor.constraint(equalToConstant: 120)
  1450. ])
  1451. containerActionGroup.backgroundColor = .secondaryColor.withAlphaComponent(0.8)
  1452. containerActionGroup.addSubview(labelKicked)
  1453. labelKicked.translatesAutoresizingMaskIntoConstraints = false
  1454. NSLayoutConstraint.activate([
  1455. labelKicked.centerYAnchor.constraint(equalTo: containerActionGroup.centerYAnchor),
  1456. labelKicked.centerXAnchor.constraint(equalTo: containerActionGroup.centerXAnchor),
  1457. ])
  1458. labelKicked.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  1459. labelKicked.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  1460. if contactChatNav.viewIfLoaded?.window != nil {
  1461. contactChatNav.dismiss(animated: true)
  1462. }
  1463. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
  1464. if self.fromNotification {
  1465. self.didTapExit()
  1466. } else {
  1467. self.navigationController?.popViewController(animated: true)
  1468. }
  1469. })
  1470. })
  1471. }
  1472. }
  1473. }
  1474. @objc func onGroup(notification: NSNotification) {
  1475. let data:[AnyHashable : Any] = notification.userInfo!
  1476. if data["code"] as? String ?? "" == "A010" && data["groupId"] as? String ?? "" == self.dataGroup["group_id"] as? String ?? "" {
  1477. DispatchQueue.main.async {
  1478. Database.shared.database?.inTransaction({ fmdb, rollback in
  1479. if let c = Database().getRecords(fmdb: fmdb, query: "select f_name, image_id from GROUPZ where group_id = '\(self.dataGroup["group_id"]!!)'"), c.next() {
  1480. self.dataGroup["f_name"] = c.string(forColumnIndex: 0)!.trimmingCharacters(in: .whitespacesAndNewlines)
  1481. self.dataGroup["image_id"] = c.string(forColumnIndex: 1)!
  1482. c.close()
  1483. }
  1484. })
  1485. self.changeAppBar()
  1486. }
  1487. }
  1488. }
  1489. @IBAction func voiceTapped(_ sender: UIButton) {
  1490. if (self.constraintBottomAttachment.constant != 0.0) {
  1491. constraintBottomAttachment.constant = 0.0
  1492. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1493. self.viewSticker.removeFromSuperview()
  1494. }
  1495. }
  1496. @IBAction func imageTapped(_ sender: UIButton) {
  1497. if (self.constraintBottomAttachment.constant != 0.0) {
  1498. constraintBottomAttachment.constant = 0.0
  1499. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1500. self.viewSticker.removeFromSuperview()
  1501. }
  1502. let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
  1503. if let action = self.actionImageVideo(for: "image", title: "Choose Photo".localized()) {
  1504. alertController.addAction(action)
  1505. }
  1506. if let action = self.actionImageVideo(for: "video", title: "Choose Video".localized()) {
  1507. alertController.addAction(action)
  1508. }
  1509. alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
  1510. self.present(alertController, animated: true)
  1511. }
  1512. private func actionImageVideo(for type: String, title: String) -> UIAlertAction? {
  1513. return UIAlertAction(title: title, style: .default) { [unowned self] _ in
  1514. switch type {
  1515. case "image":
  1516. var config = PHPickerConfiguration()
  1517. config.filter = .images
  1518. config.preferredAssetRepresentationMode = .automatic
  1519. let picker = PHPickerViewController(configuration: config)
  1520. picker.delegate = self
  1521. if UIBarButtonItem.appearance().titleTextAttributes(for: .normal) != nil {
  1522. isBlackCancelButton = UIBarButtonItem.appearance().titleTextAttributes(for: .normal)?.values.first as! NSObject == UIColor.black
  1523. }
  1524. if !isBlackCancelButton {
  1525. let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
  1526. UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
  1527. }
  1528. present(picker, animated: true, completion: nil)
  1529. case "video":
  1530. var config = PHPickerConfiguration()
  1531. config.filter = .videos
  1532. config.preferredAssetRepresentationMode = .automatic
  1533. let picker = PHPickerViewController(configuration: config)
  1534. picker.delegate = self
  1535. if UIBarButtonItem.appearance().titleTextAttributes(for: .normal) != nil {
  1536. isBlackCancelButton = UIBarButtonItem.appearance().titleTextAttributes(for: .normal)?.values.first as! NSObject == UIColor.black
  1537. }
  1538. if !isBlackCancelButton {
  1539. let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
  1540. UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
  1541. }
  1542. present(picker, animated: true, completion: nil)
  1543. case "imageCamera":
  1544. imageVideoPicker.present(source: .imageCamera)
  1545. case "videoCamera":
  1546. imageVideoPicker.present(source: .videoCamera)
  1547. default:
  1548. imageVideoPicker.present(source: .imageAlbum)
  1549. }
  1550. }
  1551. }
  1552. @IBAction func photoTapped(_ sender: UIButton) {
  1553. if (self.constraintBottomAttachment.constant != 0.0) {
  1554. constraintBottomAttachment.constant = 0.0
  1555. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1556. self.viewSticker.removeFromSuperview()
  1557. }
  1558. let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
  1559. if let action = self.actionImageVideo(for: "imageCamera", title: "Take Photo".localized()) {
  1560. alertController.addAction(action)
  1561. }
  1562. if let action = self.actionImageVideo(for: "videoCamera", title: "Take Video".localized()) {
  1563. alertController.addAction(action)
  1564. }
  1565. alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
  1566. self.present(alertController, animated: true)
  1567. }
  1568. @IBAction func stickerTapped(_ sender: UIButton) {
  1569. if textFieldSend.isFirstResponder {
  1570. dismissKeyboard()
  1571. }
  1572. DispatchQueue.main.async {
  1573. if !self.viewSticker.isDescendant(of: self.view) {
  1574. self.constraintBottomAttachment.constant = 200.0
  1575. self.view.addSubview(self.viewSticker)
  1576. self.viewSticker.translatesAutoresizingMaskIntoConstraints = false
  1577. NSLayoutConstraint.activate([
  1578. self.viewSticker.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
  1579. self.viewSticker.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
  1580. self.viewSticker.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
  1581. self.viewSticker.heightAnchor.constraint(equalToConstant: 200)
  1582. ])
  1583. let layout = UICollectionViewFlowLayout()
  1584. layout.scrollDirection = .vertical
  1585. let collectionSticker = UICollectionView(frame: .zero, collectionViewLayout: layout)
  1586. collectionSticker.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellSticker")
  1587. collectionSticker.delegate = self
  1588. collectionSticker.dataSource = self
  1589. collectionSticker.backgroundColor = .clear
  1590. self.viewSticker.addSubview(collectionSticker)
  1591. collectionSticker.translatesAutoresizingMaskIntoConstraints = false
  1592. NSLayoutConstraint.activate([
  1593. collectionSticker.topAnchor.constraint(equalTo: self.viewSticker.topAnchor, constant: 20),
  1594. collectionSticker.bottomAnchor.constraint(equalTo: self.viewSticker.bottomAnchor, constant: -20),
  1595. collectionSticker.leadingAnchor.constraint(equalTo: self.viewSticker.leadingAnchor, constant: 20),
  1596. collectionSticker.trailingAnchor.constraint(equalTo: self.viewSticker.trailingAnchor, constant: -20)
  1597. ])
  1598. if (self.currentIndexpath != nil) {
  1599. DispatchQueue.main.async {
  1600. self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
  1601. }
  1602. } else {
  1603. self.tableChatView.scrollToBottom()
  1604. }
  1605. } else {
  1606. self.constraintBottomAttachment.constant = 0.0
  1607. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1608. self.viewSticker.removeFromSuperview()
  1609. }
  1610. }
  1611. }
  1612. @IBAction func fileTapped(_ sender: UIButton) {
  1613. if (self.constraintBottomAttachment.constant != 0.0) {
  1614. constraintBottomAttachment.constant = 0.0
  1615. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1616. self.viewSticker.removeFromSuperview()
  1617. }
  1618. documentPicker.present()
  1619. }
  1620. @objc func didTapExit() {
  1621. self.dismiss(animated: true, completion: {
  1622. self.removeAllObjectBeforeDismissVC()
  1623. })
  1624. }
  1625. @objc func profilePersonTapped(_ sender: ObjectGesture) {
  1626. if isHistoryCC {
  1627. return
  1628. }
  1629. let idMe = User.getMyPin() as String?
  1630. if sender.message_id == idMe {
  1631. let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
  1632. controller.data = sender.message_id
  1633. controller.flag = .me
  1634. navigationController?.show(controller, sender: nil)
  1635. } else {
  1636. let data = User.getDataCanNil(pin: sender.message_id)
  1637. if data != nil {
  1638. let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
  1639. controller.flag = .friend
  1640. controller.user = data
  1641. controller.name = data!.fullName
  1642. controller.data = sender.message_id
  1643. controller.picture = data!.thumb
  1644. self.navigationController?.show(controller, sender: nil)
  1645. } else {
  1646. let dataUser = getDataProfile(f_pin: sender.message_id, message_id: "")
  1647. let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
  1648. controller.flag = .invite
  1649. controller.user = nil
  1650. controller.name = dataUser["name"]!
  1651. controller.data = sender.message_id
  1652. controller.picture = dataUser["image_id"]!
  1653. self.navigationController?.show(controller, sender: nil)
  1654. }
  1655. }
  1656. }
  1657. @objc func seeProfileTapped() {
  1658. if isHistoryCC || removed || copySession || forwardSession || deleteSession || (dataGroup["official"] as? String == "1" && (dataGroup["parent"] as? String)!.isEmpty) {
  1659. return
  1660. }
  1661. dismissKeyboard()
  1662. let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "groupDetailView") as! GroupDetailViewController
  1663. controller.data = dataGroup["group_id"] as? String ?? ""
  1664. controller.checkReadMessage = {
  1665. if self.currentIndexpath == nil {
  1666. var listData = self.dataMessages
  1667. listData = listData.filter({$0["status"] as? String ?? "" != "4" && $0["status"] as? String ?? "" != "8"})
  1668. if listData.count != 0 {
  1669. let idMe = User.getMyPin() as String?
  1670. for i in 0...listData.count - 1 {
  1671. if listData[i]["f_pin"] as? String != idMe {
  1672. self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"] as? String ?? "", f_pin: listData[i]["f_pin"] as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: listData[i]["message_id"] as? String ?? "")
  1673. }
  1674. }
  1675. }
  1676. } else {
  1677. let dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[self.currentIndexpath!.section] })
  1678. var listData = dataMessages
  1679. listData = listData.filter({$0["status"] as? String ?? "" != "4" && $0["status"] as? String ?? "" != "8"})
  1680. if listData.count != 0 {
  1681. let idMe = User.getMyPin() as String?
  1682. for i in 0...listData.count - 1 {
  1683. if listData[i]["f_pin"] as? String != idMe {
  1684. self.sendReadMessageStatus(chat_id: self.dataTopic["chat_id"] as? String ?? "", f_pin: listData[i]["f_pin"] as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: listData[i]["message_id"] as? String ?? "")
  1685. }
  1686. }
  1687. }
  1688. }
  1689. }
  1690. navigationController?.show(controller, sender: nil)
  1691. }
  1692. @objc func dismissKeyboard() {
  1693. if isSearching {
  1694. searchBar.resignFirstResponder()
  1695. } else {
  1696. textFieldSend.resignFirstResponder() // dismiss keyoard
  1697. if (self.constraintBottomAttachment.constant != 0.0) {
  1698. constraintBottomAttachment.constant = 0.0
  1699. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1700. self.viewSticker.removeFromSuperview()
  1701. }
  1702. }
  1703. }
  1704. @objc func keyboardWillShow(notification: NSNotification) {
  1705. if self.viewIfLoaded?.window != nil && !isEditingMessage {
  1706. if (self.constraintBottomAttachment.constant != 0.0) {
  1707. self.constraintBottomAttachment.constant = 0.0
  1708. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  1709. self.viewSticker.removeFromSuperview()
  1710. }
  1711. let info:NSDictionary = notification.userInfo! as NSDictionary
  1712. let keyboardSize = (info[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
  1713. let keyboardHeight: CGFloat = keyboardSize.height
  1714. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  1715. if self.constraintBottomAttachment.constant != keyboardHeight || self.constraintViewTextField.constant != keyboardHeight - 60 {
  1716. // self.constraintViewTextField.constant = keyboardHeight - 60
  1717. self.constraintBottomAttachment.constant = keyboardHeight
  1718. if self.contraintBottomMention.constant > 0 {
  1719. self.contraintBottomMention.constant = 25 + constraintBottomAttachment.constant + self.heightTextFieldSend.constant + self.viewTextfield.bounds.height
  1720. }
  1721. self.keyboardHeightForMention = keyboardHeight
  1722. if isSearching {
  1723. self.constraintBottomContainerMultpileSelectSession.constant = -keyboardHeight
  1724. }
  1725. UIView.animate(withDuration: TimeInterval(duration), animations: {
  1726. self.view.layoutIfNeeded()
  1727. })
  1728. if (self.currentIndexpath != nil) {
  1729. self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
  1730. } else {
  1731. self.tableChatView.scrollToBottom()
  1732. }
  1733. }
  1734. } else if isEditingMessage {
  1735. let info:NSDictionary = notification.userInfo! as NSDictionary
  1736. let keyboardSize = (info[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
  1737. let keyboardHeight: CGFloat = keyboardSize.height
  1738. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  1739. let constant: CGFloat = 0 - keyboardHeight - 15
  1740. constraintBottomeditTextView.constant = constant
  1741. constraintBottomSendEditTV.constant = constant
  1742. UIView.animate(withDuration: TimeInterval(duration), animations: {
  1743. self.view.layoutIfNeeded()
  1744. })
  1745. }
  1746. }
  1747. @objc func keyboardWillHide(notification: NSNotification) {
  1748. if self.viewIfLoaded?.window != nil && !isEditingMessage {
  1749. let info:NSDictionary = notification.userInfo! as NSDictionary
  1750. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  1751. self.constraintViewTextField.constant = 0
  1752. self.constraintBottomAttachment.constant = 0
  1753. self.constraintBottomContainerMultpileSelectSession.constant = 0
  1754. if self.contraintBottomMention.constant > 0 {
  1755. self.contraintBottomMention.constant = 25 + constraintBottomAttachment.constant + self.heightTextFieldSend.constant + self.viewTextfield.bounds.height
  1756. }
  1757. keyboardHeightForMention = nil
  1758. UIView.animate(withDuration: TimeInterval(duration), animations: {
  1759. self.view.layoutIfNeeded()
  1760. })
  1761. }
  1762. }
  1763. @objc func showChooserACKConfidential() {
  1764. // dismissKeyboard()
  1765. let alertController = LibAlertController(title: "Message Mode".localized(), message: "Select".localized() + " " + "Message Mode".localized(), preferredStyle: .actionSheet)
  1766. let imageConfidential = resizeImage(image: UIImage(named: "pb_icon_conf_msg_on", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
  1767. let imageAck = resizeImage(image: UIImage(named: "pb_icon_ack_msg_on", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
  1768. let imageSticker = resizeImage(image: UIImage(named: "Sticker---Emoji", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
  1769. let confidentialAction = UIAlertAction(title: "Confidential Message".localized(), style: .default, handler: { (UIAlertAction) in
  1770. self.isConfidential = !self.isConfidential
  1771. if self.isConfidential {
  1772. self.buttonAckConfidential.setImage(imageConfidential, for: .normal)
  1773. } else {
  1774. self.buttonAckConfidential.setImage(UIImage(systemName: "gearshape.fill", withConfiguration: UIImage.SymbolConfiguration(scale: .large))?.withTintColor(.white).withRenderingMode(.alwaysTemplate), for: .normal)
  1775. }
  1776. if self.isAck {
  1777. self.isAck = false
  1778. }
  1779. })
  1780. let ackAction = UIAlertAction(title: "Confirmation Message".localized(), style: .default, handler: { (UIAlertAction) in
  1781. self.isAck = !self.isAck
  1782. if self.isAck {
  1783. self.buttonAckConfidential.setImage(imageAck, for: .normal)
  1784. } else {
  1785. self.buttonAckConfidential.setImage(UIImage(systemName: "gearshape.fill", withConfiguration: UIImage.SymbolConfiguration(scale: .large))?.withTintColor(.white).withRenderingMode(.alwaysTemplate), for: .normal)
  1786. }
  1787. if self.isConfidential {
  1788. self.isConfidential = false
  1789. }
  1790. })
  1791. let stickerAction = UIAlertAction(title: "Open Sticker".localized(), style: .default, handler: { (UIAlertAction) in
  1792. self.stickerTapped(UIButton())
  1793. })
  1794. confidentialAction.setValue(imageConfidential, forKey: "image")
  1795. ackAction.setValue(imageAck, forKey: "image")
  1796. stickerAction.setValue(imageSticker, forKey: "image")
  1797. alertController.addAction(confidentialAction)
  1798. alertController.addAction(ackAction)
  1799. // alertController.addAction(stickerAction)
  1800. alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { (UIAlertAction) in
  1801. self.isConfidential = false
  1802. self.isAck = false
  1803. self.buttonAckConfidential.setImage(UIImage(systemName: "gearshape.fill", withConfiguration: UIImage.SymbolConfiguration(scale: .large))?.withTintColor(.white).withRenderingMode(.alwaysTemplate), for: .normal)
  1804. }))
  1805. self.present(alertController, animated: true, completion: nil)
  1806. }
  1807. public func setAckConfidential(isAck: Bool, isConfidential: Bool) {
  1808. self.isConfidential = isConfidential
  1809. self.isAck = isAck
  1810. let imageConfidential = resizeImage(image: UIImage(named: "confidential_icon", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
  1811. let imageAck = resizeImage(image: UIImage(named: "ack_icon", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal)
  1812. if isAck {
  1813. buttonAckConfidential.setImage(imageAck, for: .normal)
  1814. } else if isConfidential {
  1815. buttonAckConfidential.setImage(imageConfidential, for: .normal)
  1816. } else {
  1817. self.buttonAckConfidential.setImage(UIImage(systemName: "gearshape.fill", withConfiguration: UIImage.SymbolConfiguration(scale: .large))?.withTintColor(.white).withRenderingMode(.alwaysTemplate), for: .normal)
  1818. }
  1819. }
  1820. @objc func sendTapped() {
  1821. sendChat(message_text: textFieldSend.text!, viewController: self)
  1822. }
  1823. private func sendChat(message_scope_id:String = MessageScope.GROUP, status:String = "1", message_text:String = "", credential:String = "0", attachment_flag: String = "0", ex_blog_id: String = "", message_large_text: String = "", ex_format: String = "", image_id: String = "", audio_id: String = "", video_id: String = "", file_id: String = "", thumb_id: String = "", reff_id: String = "", read_receipts: String = "", is_call_center: String = "0", call_center_id: String = "", viewController: UIViewController, gif_id: String = "", is_forwarded: Int = 0) {
  1824. if viewController is EditorGroup && file_id == "" && dataMessageForward == nil {
  1825. if ((textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines) == "Send message".localized() && textFieldSend.textColor == UIColor.lightGray && attachment_flag != "11") || textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) {
  1826. dismissKeyboard()
  1827. viewController.view.makeToast("Write Messages".localized(), duration: 3)
  1828. if (textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines) != "Send message".localized()) {
  1829. textFieldSend.text = ""
  1830. }
  1831. if (self.heightTextFieldSend.constant != 40) {
  1832. self.heightTextFieldSend.constant = 40
  1833. }
  1834. return
  1835. }
  1836. }
  1837. var reff_id = reff_id
  1838. if (reffId != nil) {
  1839. reff_id = reffId!
  1840. }
  1841. var message_text = message_text
  1842. let bulletPoint = " • "
  1843. let numberPattern = #" \d+\.\ "#
  1844. let firstLine = message_text.components(separatedBy: .newlines).first ?? ""
  1845. // Check if text contains bullet points or numbered list using regex
  1846. if !message_text.isEmpty && !firstLine.contains(bulletPoint) && firstLine.range(of: numberPattern, options: .regularExpression) == nil {
  1847. message_text = message_text.trimmingCharacters(in: .whitespacesAndNewlines)
  1848. }
  1849. let idMe = User.getMyPin() as String?
  1850. var opposite_pin = self.dataGroup["group_id"] as? String ?? ""
  1851. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  1852. opposite_pin = self.dataTopic["chat_id"] as? String ?? ""
  1853. }
  1854. var credential = credential
  1855. if isConfidential {
  1856. credential = "1"
  1857. }
  1858. var read_receipts = read_receipts
  1859. if isAck {
  1860. read_receipts = "8"
  1861. }
  1862. if message_text.contains("@") && listMentionInTextField.count > 0 {
  1863. var diff: Int = 0
  1864. for i in 0..<listMentionInTextField.count {
  1865. let mention = listMentionInTextField[i]
  1866. guard let exBlockStr = mention.ex_block, let exBlock = Int(exBlockStr) else {
  1867. continue // skip if ex_block is nil or not an integer
  1868. }
  1869. let nameWithMention = ("@" + mention.fullName).trimmingCharacters(in: .whitespaces)
  1870. let pinString = "@\(mention.pin)"
  1871. let upperBound = exBlock + diff
  1872. let lowerBound = upperBound - nameWithMention.count + 1
  1873. guard lowerBound >= 0, upperBound < message_text.count else {
  1874. continue // prevent index out-of-range
  1875. }
  1876. var afterMention = ""
  1877. let nextCharIndex = message_text.index(message_text.startIndex, offsetBy: upperBound + 1, limitedBy: message_text.endIndex)
  1878. if let index = nextCharIndex, index < message_text.endIndex {
  1879. let nextChar = message_text[index]
  1880. if nextChar != "\n" && nextChar != " " {
  1881. afterMention = " "
  1882. }
  1883. }
  1884. let startIndex = message_text.index(message_text.startIndex, offsetBy: lowerBound)
  1885. let endIndex = message_text.index(message_text.startIndex, offsetBy: upperBound + 1)
  1886. let range = startIndex..<endIndex
  1887. if message_text[range] == nameWithMention {
  1888. message_text.replaceSubrange(range, with: pinString + afterMention)
  1889. diff += (pinString + afterMention).count - nameWithMention.count
  1890. }
  1891. }
  1892. }
  1893. if Nexilis.checkingAccess(key: "message_guard") {
  1894. let guardLite = MessageGuardLite(limits: .defaults())
  1895. var isSanitizedText = false
  1896. var isSanitizedHtml = false
  1897. let res = guardLite.sanitizeText(message_text.data(using: .utf8)!)
  1898. if res.verdict == .sanitized {
  1899. isSanitizedText = true
  1900. }
  1901. if let clean = res.data, let str = String(data: clean, encoding: .utf8) {
  1902. if MessageGuardLite.containsHtmlTags(str) {
  1903. let res2 = guardLite.sanitizeHtml(res.data ?? Data())
  1904. if res2.verdict == .sanitized {
  1905. isSanitizedHtml = true
  1906. }
  1907. if let clean2 = res.data, let str2 = String(data: clean, encoding: .utf8) {
  1908. message_text = str2
  1909. }
  1910. } else {
  1911. message_text = str
  1912. }
  1913. }
  1914. var protectionType = ""
  1915. if isSanitizedText && isSanitizedHtml {
  1916. protectionType = "text & html"
  1917. } else if isSanitizedText {
  1918. protectionType = "text"
  1919. } else if isSanitizedHtml {
  1920. protectionType = "html"
  1921. }
  1922. if !protectionType.isEmpty {
  1923. DispatchQueue.main.async {
  1924. self.view.makeToast("Your message is protected with sanitized \(protectionType) (Message Guard)".localized(), duration: 3)
  1925. }
  1926. }
  1927. }
  1928. let message = CoreMessage_TMessageBank.sendMessage(l_pin: dataGroup["group_id"] as? String ?? "", message_scope_id: message_scope_id, status: status, message_text: message_text, credential: credential, attachment_flag: attachment_flag, ex_blog_id: ex_blog_id, message_large_text: message_large_text, ex_format: ex_format, image_id: image_id, audio_id: audio_id, video_id: video_id, file_id: file_id, thumb_id: thumb_id, reff_id: reff_id, read_receipts: read_receipts, chat_id: dataTopic["chat_id"] as? String ?? "", is_call_center: is_call_center, call_center_id: call_center_id, opposite_pin: opposite_pin, gif_id: gif_id, isForwarded: "\(is_forwarded)", specFile: specFileString)
  1929. Nexilis.addQueueMessage(message: message)
  1930. let messageId = String(message.mBodies[CoreMessage_TMessageKey.MESSAGE_ID]!)
  1931. if credential == "1" {
  1932. self.listTimerCredential[messageId] = 60
  1933. }
  1934. var row: [String: Any?] = [:]
  1935. row["message_id"] = messageId
  1936. row["f_pin"] = idMe
  1937. row["l_pin"] = dataGroup["group_id"]!!
  1938. row["message_scope_id"] = message_scope_id
  1939. row["server_date"] = "\(Date().currentTimeMillis())"
  1940. row["status"] = status
  1941. row["message_text"] = message_text
  1942. row["audio_id"] = audio_id
  1943. row["video_id"] = video_id
  1944. row["image_id"] = image_id
  1945. row["thumb_id"] = thumb_id
  1946. row["credential"] = credential
  1947. row["read_receipts"] = read_receipts
  1948. row["chat_id"] = dataTopic["chat_id"]!!
  1949. row["file_id"] = file_id
  1950. row["attachment_flag"] = attachment_flag
  1951. row["reff_id"] = reff_id
  1952. row["progress"] = 0.0
  1953. row["lock"] = "0"
  1954. row["is_stared"] = "0"
  1955. row["isSelected"] = false
  1956. row["gif_id"] = gif_id
  1957. row[TypeDataMessage.is_forwarded] = is_forwarded
  1958. row[TypeDataMessage.is_call_center] = is_call_center
  1959. row[TypeDataMessage.call_center_id] = call_center_id
  1960. row[TypeDataMessage.opposite_pin] = opposite_pin
  1961. row[TypeDataMessage.spec_file] = specFileString
  1962. specFileString = ""
  1963. lastTextLength = 0
  1964. if !dataDates.contains("Today".localized()){
  1965. dataDates.append("Today".localized())
  1966. tableChatView.insertSections(IndexSet(integer: dataDates.count - 1), with: .fade)
  1967. }
  1968. row["chat_date"] = "Today".localized()
  1969. self.tableChatView.beginUpdates()
  1970. dataMessages.append(row)
  1971. tableChatView.insertRows(at: [IndexPath(row: dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[dataDates.count - 1]}).count - 1, section: dataDates.count - 1)], with: .fade)
  1972. self.tableChatView.endUpdates()
  1973. if credential == "1" {
  1974. var timer = Timer()
  1975. var minute = 60
  1976. timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in
  1977. minute -= 1
  1978. self.listTimerCredential[messageId] = minute
  1979. if minute == 0 {
  1980. timer.invalidate()
  1981. self.listTimerCredential.removeValue(forKey: messageId)
  1982. self.timerCredential.removeValue(forKey: messageId)
  1983. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == messageId})
  1984. if idx != nil {
  1985. self.dataMessages[idx!]["lock"] = "2"
  1986. self.dataMessages[idx!]["reff_id"] = ""
  1987. }
  1988. DispatchQueue.global().async {
  1989. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  1990. do {
  1991. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  1992. "lock" : "2"
  1993. ], _where: "message_id = '\(messageId)'")
  1994. } catch {
  1995. rollback.pointee = true
  1996. print("Access database error: \(error.localizedDescription)")
  1997. }
  1998. })
  1999. }
  2000. }
  2001. let section = self.dataDates.firstIndex(of: self.dataDates[self.dataDates.count - 1])
  2002. let row = self.dataMessages.filter({$0["chat_date"] as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).firstIndex(where: { $0["message_id"] as? String == messageId})
  2003. if row != nil && section != nil{
  2004. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  2005. }
  2006. })
  2007. self.timerCredential[messageId] = timer
  2008. }
  2009. if textFieldSend.text!.trimmingCharacters(in: .whitespacesAndNewlines) != "Send message".localized() && textFieldSend.textColor != UIColor.lightGray && constraintBottomAttachment.constant == 0 {
  2010. textFieldSend.text = "Send message".localized()
  2011. textFieldSend.textColor = UIColor.lightGray
  2012. } else if constraintBottomAttachment.constant != 0 {
  2013. textFieldSend.text = ""
  2014. heightTextFieldSend.constant = 40
  2015. }
  2016. deleteReplyView()
  2017. deleteLinkPreview()
  2018. listMentionInTextField.removeAll()
  2019. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  2020. self.tableChatView.scrollToBottom()
  2021. if self.markerCounter != nil {
  2022. let lastMarkerCounter = self.markerCounter
  2023. self.markerCounter = nil
  2024. let indexMessage = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == lastMarkerCounter })
  2025. if indexMessage != nil {
  2026. let section = self.dataDates.firstIndex(of: self.dataMessages[indexMessage!]["chat_date"] as? String ?? "")
  2027. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[indexMessage!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[indexMessage!]["message_id"] as? String })
  2028. if row != nil && section != nil {
  2029. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  2030. }
  2031. }
  2032. }
  2033. }
  2034. private func getCounter() {
  2035. Database.shared.database?.inTransaction({ fmdb, rollback in
  2036. var l_pin = self.dataGroup["group_id"] as? String ?? ""
  2037. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  2038. l_pin = self.dataTopic["chat_id"] as? String ?? ""
  2039. }
  2040. if let c = Database().getRecords(fmdb: fmdb, query: "SELECT counter FROM MESSAGE_SUMMARY where l_pin='\(l_pin)'"), c.next() {
  2041. counter = Int(c.int(forColumnIndex: 0))
  2042. c.close()
  2043. }
  2044. })
  2045. }
  2046. private func updateCounter(counter: Int) {
  2047. DispatchQueue.global().async {
  2048. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  2049. do {
  2050. var l_pin = self.dataGroup["group_id"]!!
  2051. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  2052. l_pin = self.dataTopic["chat_id"] as? String ?? ""
  2053. }
  2054. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", cvalues: [
  2055. "counter" : "\(counter)"
  2056. ], _where: "l_pin = '\(l_pin)'")
  2057. } catch {
  2058. rollback.pointee = true
  2059. print("Access database error: \(error.localizedDescription)")
  2060. }
  2061. })
  2062. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  2063. }
  2064. }
  2065. private func disableEditor() {
  2066. view.addSubview(containerAction)
  2067. containerAction.translatesAutoresizingMaskIntoConstraints = false
  2068. NSLayoutConstraint.activate([
  2069. containerAction.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
  2070. containerAction.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
  2071. containerAction.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
  2072. containerAction.heightAnchor.constraint(equalToConstant: 120)
  2073. ])
  2074. containerAction.backgroundColor = .secondaryColor.withAlphaComponent(0.8)
  2075. let labelDisable = UILabel()
  2076. containerAction.addSubview(labelDisable)
  2077. labelDisable.translatesAutoresizingMaskIntoConstraints = false
  2078. NSLayoutConstraint.activate([
  2079. labelDisable.centerYAnchor.constraint(equalTo: containerAction.centerYAnchor),
  2080. labelDisable.centerXAnchor.constraint(equalTo: containerAction.centerXAnchor),
  2081. ])
  2082. labelDisable.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  2083. labelDisable.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  2084. labelDisable.text = "Call Center Session has ended".localized()
  2085. }
  2086. private func addButtonScrollToBottom() {
  2087. if tableChatView.alpha != 1 || isSearching {
  2088. return
  2089. }
  2090. self.view.addSubview(buttonScrollToBottom)
  2091. buttonScrollToBottom.translatesAutoresizingMaskIntoConstraints = false
  2092. NSLayoutConstraint.activate([
  2093. buttonScrollToBottom.bottomAnchor.constraint(equalTo: buttonSendChat.topAnchor, constant: -50),
  2094. buttonScrollToBottom.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
  2095. buttonScrollToBottom.widthAnchor.constraint(equalToConstant: 60),
  2096. buttonScrollToBottom.heightAnchor.constraint(equalToConstant: 30.0)
  2097. ])
  2098. buttonScrollToBottom.backgroundColor = .greenColor
  2099. buttonScrollToBottom.setImage(UIImage(systemName: "chevron.down.circle"), for: .normal)
  2100. buttonScrollToBottom.imageView?.contentMode = .scaleAspectFit
  2101. buttonScrollToBottom.imageView?.tintColor = .white
  2102. buttonScrollToBottom.contentVerticalAlignment = .fill
  2103. buttonScrollToBottom.contentHorizontalAlignment = .fill
  2104. buttonScrollToBottom.imageEdgeInsets.top = 2.0
  2105. buttonScrollToBottom.imageEdgeInsets.bottom = 2.0
  2106. buttonScrollToBottom.layer.cornerRadius = 10.0
  2107. buttonScrollToBottom.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
  2108. buttonScrollToBottom.clipsToBounds = true
  2109. buttonScrollToBottom.addTarget(self, action: #selector(scrollTobottomAction), for: .touchUpInside)
  2110. }
  2111. private func addCounterAtButttonScrollToBottom() {
  2112. if tableChatView.alpha != 1 || isSearching {
  2113. return
  2114. }
  2115. self.view.addSubview(indicatorCounterBSTB)
  2116. indicatorCounterBSTB.translatesAutoresizingMaskIntoConstraints = false
  2117. indicatorCounterBSTB.backgroundColor = .systemRed
  2118. indicatorCounterBSTB.layer.cornerRadius = 7.5
  2119. indicatorCounterBSTB.clipsToBounds = true
  2120. indicatorCounterBSTB.layer.borderWidth = 0.5
  2121. indicatorCounterBSTB.layer.borderColor = UIColor.secondaryColor.cgColor
  2122. NSLayoutConstraint.activate([
  2123. indicatorCounterBSTB.bottomAnchor.constraint(equalTo: buttonScrollToBottom.topAnchor, constant: 5),
  2124. indicatorCounterBSTB.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -50),
  2125. indicatorCounterBSTB.widthAnchor.constraint(greaterThanOrEqualToConstant: 15),
  2126. indicatorCounterBSTB.heightAnchor.constraint(equalToConstant: 15)
  2127. ])
  2128. indicatorCounterBSTB.addSubview(labelCounter)
  2129. labelCounter.translatesAutoresizingMaskIntoConstraints = false
  2130. NSLayoutConstraint.activate([
  2131. labelCounter.leadingAnchor.constraint(equalTo: indicatorCounterBSTB.leadingAnchor, constant: 2),
  2132. labelCounter.trailingAnchor.constraint(equalTo: indicatorCounterBSTB.trailingAnchor, constant: -2),
  2133. labelCounter.centerXAnchor.constraint(equalTo: indicatorCounterBSTB.centerXAnchor),
  2134. ])
  2135. labelCounter.font = UIFont.systemFont(ofSize: 11 + offset())
  2136. labelCounter.text = "\(counter)"
  2137. labelCounter.textColor = .secondaryColor
  2138. labelCounter.textAlignment = .center
  2139. }
  2140. @objc func scrollTobottomAction() {
  2141. tableChatView.scrollToBottom()
  2142. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [self] in
  2143. if buttonScrollToBottom.isDescendant(of: self.view) {
  2144. buttonScrollToBottom.removeConstraints(buttonScrollToBottom.constraints)
  2145. buttonScrollToBottom.removeFromSuperview()
  2146. if indicatorCounterBSTB.isDescendant(of: self.view) {
  2147. indicatorCounterBSTB.removeConstraints(indicatorCounterBSTB.constraints)
  2148. indicatorCounterBSTB.removeFromSuperview()
  2149. }
  2150. }
  2151. }
  2152. }
  2153. private func sendReadMessageStatus(chat_id: String, f_pin: String, message_scope_id: String, message_id: String) {
  2154. let message = CoreMessage_TMessageBank.getUpdateRead(p_chat_id: chat_id, p_f_pin: f_pin, p_scope_id: message_scope_id, qty: 1)
  2155. let fPin = message.getBody(key: CoreMessage_TMessageKey.F_PIN)
  2156. let scope = message.getBody(key: CoreMessage_TMessageKey.SCOPE_ID)
  2157. message.mBodies[CoreMessage_TMessageKey.SERVER_DATE] = String(Date().currentTimeMillis())
  2158. if let listGroupImages = self.groupImages.first(where: { $0.key == message_id }) {
  2159. let valueListGroupImages = listGroupImages.value
  2160. message.mStatus = CoreMessage_TMessageUtil.getTID()
  2161. message.mBodies[CoreMessage_TMessageKey.L_PIN] = f_pin
  2162. var mId = ""
  2163. for i in 0..<valueListGroupImages.count {
  2164. if mId.isEmpty {
  2165. mId = "-2,\(valueListGroupImages[i].messageId)"
  2166. } else {
  2167. mId = mId + "," + valueListGroupImages[i].messageId
  2168. }
  2169. }
  2170. message.mBodies[CoreMessage_TMessageKey.MESSAGE_ID] = mId
  2171. } else {
  2172. message.mStatus = CoreMessage_TMessageUtil.getTID()
  2173. message.mBodies[CoreMessage_TMessageKey.L_PIN] = f_pin
  2174. message.mBodies[CoreMessage_TMessageKey.MESSAGE_ID] = "-2,\(message_id)"
  2175. }
  2176. if (fPin.elementsEqual("-999") || scope.elementsEqual("16") || scope.elementsEqual("15")){
  2177. return
  2178. }
  2179. DispatchQueue.global().async {
  2180. var isBackground = true
  2181. while isBackground {
  2182. DispatchQueue.main.sync {
  2183. isBackground = API.nGetCLXConnState() == 0 || !API.bInetConnAvailable() || APIS.checkAppStateisBackground()
  2184. }
  2185. if isBackground {
  2186. Thread.sleep(forTimeInterval: 1.0)
  2187. } else {
  2188. if let resp = Nexilis.writeAndWait(message: message) {
  2189. if resp.isOk() {
  2190. if let listGroupImages = self.groupImages.first(where: { $0.key == message_id }) {
  2191. let valueListGroupImages = listGroupImages.value
  2192. for i in 0..<valueListGroupImages.count {
  2193. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  2194. do {
  2195. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  2196. "status" : "4"
  2197. ], _where: "message_id = '\(valueListGroupImages[i].messageId)'")
  2198. } catch {
  2199. rollback.pointee = true
  2200. print("Access database error: \(error.localizedDescription)")
  2201. }
  2202. })
  2203. }
  2204. } else {
  2205. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  2206. do {
  2207. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  2208. "status" : "4"
  2209. ], _where: "message_id = '\(message_id)'")
  2210. } catch {
  2211. rollback.pointee = true
  2212. print("Access database error: \(error.localizedDescription)")
  2213. }
  2214. })
  2215. }
  2216. } else {
  2217. DispatchQueue.main.sync {
  2218. self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
  2219. }
  2220. }
  2221. } else {
  2222. DispatchQueue.main.sync {
  2223. self.sendReadMessageStatus(chat_id: chat_id, f_pin: fPin, message_scope_id: message_scope_id, message_id: message_id)
  2224. }
  2225. }
  2226. }
  2227. }
  2228. }
  2229. if let index = dataMessages.firstIndex(where: {$0["message_id"] as? String == message_id}) {
  2230. dataMessages[index]["status"] = "4"
  2231. let auto: Bool = SecureUserDefaults.shared.value(forKey: "autoDownload") ?? false
  2232. if auto {
  2233. if dataMessages[index]["image_id"] as? String != nil && !((dataMessages[index]["image_id"] as? String)!.isEmpty) {
  2234. Download().startHTTP(forKey:dataMessages[index]["image_id"] as? String ?? "") { (name, progress) in
  2235. guard progress == 100 else {
  2236. return
  2237. }
  2238. do {
  2239. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  2240. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  2241. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  2242. if let dirPath = paths.first {
  2243. let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(self.dataMessages[index]["image_id"] as? String ?? "")
  2244. if FileManager.default.fileExists(atPath: imageURL.path) {
  2245. let image = UIImage(contentsOfFile: imageURL.path)
  2246. let save: Bool = SecureUserDefaults.shared.value(forKey: "saveToGallery") ?? false
  2247. if save {
  2248. UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
  2249. }
  2250. }
  2251. else if FileEncryption.shared.isSecureExists(filename: self.dataMessages[index]["image_id"] as? String ?? "") {
  2252. if var secureData = try FileEncryption.shared.readSecure(filename: self.dataMessages[index]["image_id"] as? String ?? "") {
  2253. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: secureData)
  2254. if dataDecrypt != nil {
  2255. secureData = dataDecrypt!
  2256. }
  2257. let image = UIImage(data: secureData)
  2258. let save: Bool = SecureUserDefaults.shared.value(forKey: "saveToGallery") ?? false
  2259. if save {
  2260. UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
  2261. }
  2262. }
  2263. }
  2264. }
  2265. } catch {
  2266. }
  2267. DispatchQueue.main.async { [self] in
  2268. let section = dataDates.firstIndex(of: dataMessages[index]["chat_date"] as? String ?? "")
  2269. let row = dataMessages.filter({$0["chat_date"] as? String ?? "" == dataMessages[index]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == message_id})
  2270. if row != nil && section != nil{
  2271. tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .automatic)
  2272. }
  2273. }
  2274. }
  2275. } else if dataMessages[index]["video_id"] as? String != nil && !((dataMessages[index]["video_id"] as? String)!.isEmpty){
  2276. Download().startHTTP(forKey: dataMessages[index]["video_id"] as? String ?? "") { (name, progress) in
  2277. guard progress == 100 else {
  2278. return
  2279. }
  2280. do {
  2281. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  2282. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  2283. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  2284. if let dirPath = paths.first {
  2285. let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(self.dataMessages[index]["video_id"] as? String ?? "")
  2286. if FileManager.default.fileExists(atPath: videoURL.path) {
  2287. let save: Bool = SecureUserDefaults.shared.value(forKey: "saveToGallery") ?? false
  2288. if save {
  2289. PHPhotoLibrary.shared().performChanges({
  2290. PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
  2291. }) { saved, error in
  2292. }
  2293. }
  2294. }
  2295. else if FileEncryption.shared.isSecureExists(filename: self.dataMessages[index]["video_id"] as? String ?? "") {
  2296. if var secureData = try FileEncryption.shared.readSecure(filename: self.dataMessages[index]["video_id"] as? String ?? "") {
  2297. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: secureData)
  2298. if dataDecrypt != nil {
  2299. secureData = dataDecrypt!
  2300. }
  2301. let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
  2302. let tempPath = cachesDirectory.appendingPathComponent(name)
  2303. try secureData.write(to: tempPath)
  2304. let save: Bool = SecureUserDefaults.shared.value(forKey: "saveToGallery") ?? false
  2305. if save {
  2306. PHPhotoLibrary.shared().performChanges({
  2307. PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: tempPath)
  2308. }) { saved, error in
  2309. }
  2310. }
  2311. }
  2312. }
  2313. }
  2314. DispatchQueue.main.async { [self] in
  2315. let section = dataDates.firstIndex(of: dataMessages[index]["chat_date"] as? String ?? "")
  2316. let row = dataMessages.filter({$0["chat_date"] as? String ?? "" == dataMessages[index]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == message_id})
  2317. if row != nil && section != nil{
  2318. tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .automatic)
  2319. }
  2320. }
  2321. }
  2322. catch {
  2323. }
  2324. }
  2325. }
  2326. else if dataMessages[index]["file_id"] as? String != nil && !((dataMessages[index]["file_id"] as? String)!.isEmpty) {
  2327. Download().startHTTP(forKey: dataMessages[index]["file_id"] as? String ?? "") { (name, progress) in
  2328. guard progress == 100 else {
  2329. return
  2330. }
  2331. DispatchQueue.main.async { [self] in
  2332. let section = dataDates.firstIndex(of: dataMessages[index]["chat_date"] as? String ?? "")
  2333. let row = dataMessages.filter({$0["chat_date"] as? String ?? "" == dataMessages[index]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == message_id})
  2334. if row != nil && section != nil{
  2335. tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .automatic)
  2336. }
  2337. }
  2338. }
  2339. }
  2340. }
  2341. }
  2342. }
  2343. private func sendTyping(l_pin: String, isTyping: Bool = false) {
  2344. DispatchQueue.global().async {
  2345. let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: MessageScope.GROUP, p_status: isTyping ? "3": "4")
  2346. _ = Nexilis.write(message: tmessage)
  2347. }
  2348. }
  2349. private func checkNewMessage(tableView: UITableView) {
  2350. currentIndexpath = tableView.indexPathsForVisibleRows?.last
  2351. let indexFirst = tableView.indexPathsForVisibleRows?.first
  2352. if indexFirst != nil {
  2353. let dataMessages = dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexFirst!.section] })
  2354. if dataMessages.count == 0 {
  2355. return
  2356. }
  2357. let contentHeight = tableView.contentSize.height
  2358. let scrollViewHeight = tableView.frame.height
  2359. let fullContentOffset = contentHeight - scrollViewHeight
  2360. let contentOffsetY = tableView.contentOffset.y
  2361. if ((currentIndexpath!.section == dataDates.count - 1 && indexFirst!.row != dataMessages.count - 1) || indexFirst!.section != dataDates.count - 1) && fullContentOffset - contentOffsetY > 100 {
  2362. if !buttonScrollToBottom.isDescendant(of: self.view) {
  2363. addButtonScrollToBottom()
  2364. addCounterAtButttonScrollToBottom()
  2365. }
  2366. } else if (indexFirst!.section == dataDates.count - 1 && indexFirst!.row == dataMessages.count - 1) || fullContentOffset - contentOffsetY < 50 {
  2367. if buttonScrollToBottom.isDescendant(of: self.view) {
  2368. buttonScrollToBottom.removeConstraints(buttonScrollToBottom.constraints)
  2369. buttonScrollToBottom.removeFromSuperview()
  2370. if indicatorCounterBSTB.isDescendant(of: self.view) {
  2371. indicatorCounterBSTB.removeConstraints(indicatorCounterBSTB.constraints)
  2372. indicatorCounterBSTB.removeFromSuperview()
  2373. }
  2374. }
  2375. }
  2376. let indexPathFirst = tableChatView.indexPathsForVisibleRows?.first
  2377. if indexPathFirst != nil && listViewOnSection.count != 0 && listViewOnSection.count - 1 >= indexPathFirst!.section {
  2378. let headerView = listViewOnSection[indexPathFirst!.section]
  2379. if headerView.isHidden {
  2380. headerView.isHidden = false
  2381. }
  2382. }
  2383. if dataMessages.count - 1 < currentIndexpath!.row {
  2384. return
  2385. }
  2386. var listData = dataMessages[0...currentIndexpath!.row]
  2387. listData = listData.filter({$0["status"] as? String ?? "" != "4" && $0["status"] as? String ?? "" != "8"})
  2388. if listData.count != 0 {
  2389. let idMe = User.getMyPin() as String?
  2390. for i in 0...listData.count - 1 {
  2391. if listData[i]["f_pin"] as? String != idMe {
  2392. sendReadMessageStatus(chat_id: self.dataTopic["chat_id"] as? String ?? "", f_pin: listData[i]["f_pin"] as? String ?? "", message_scope_id: MessageScope.GROUP, message_id: listData[i]["message_id"] as? String ?? "")
  2393. }
  2394. }
  2395. }
  2396. }
  2397. if counter == 0 && indicatorCounterBSTB.isDescendant(of: self.view) {
  2398. indicatorCounterBSTB.removeConstraints(indicatorCounterBSTB.constraints)
  2399. indicatorCounterBSTB.removeFromSuperview()
  2400. } else if counter != 0 && currentIndexpath != nil {
  2401. let dataFilter = dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[currentIndexpath!.section] })
  2402. if dataFilter.count == 0 {
  2403. return
  2404. }
  2405. let idx = dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == dataFilter[currentIndexpath!.row]["message_id"] as? String ?? ""})
  2406. if idx == nil {
  2407. return
  2408. }
  2409. if (dataMessages.count - counter) <= idx! {
  2410. let countUpdate = idx! - (dataMessages.count - counter)
  2411. counter = counter - (countUpdate + 1)
  2412. if indicatorCounterBSTB.isDescendant(of: self.view) {
  2413. labelCounter.text = "\(counter)"
  2414. }
  2415. updateCounter(counter: counter)
  2416. }
  2417. }
  2418. }
  2419. }
  2420. extension EditorGroup: ImageVideoPickerDelegate, PreviewAttachmentImageVideoDelegate, PHPickerViewControllerDelegate {
  2421. public func didSelect(imagevideo: Any?) {
  2422. if (imagevideo != nil) {
  2423. let imageVideoData = imagevideo as! [UIImagePickerController.InfoKey: Any]
  2424. let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
  2425. previewImageVC.imageVideoData = imageVideoData
  2426. if (textFieldSend.textColor != .lightGray) {
  2427. previewImageVC.currentTextTextField = textFieldSend.text
  2428. }
  2429. previewImageVC.modalPresentationStyle = .custom
  2430. previewImageVC.delegate = self
  2431. previewImageVC.isGroup = true
  2432. previewImageVC.isAck = self.isAck
  2433. previewImageVC.isConfidential = self.isConfidential
  2434. self.present(previewImageVC, animated: true, completion: nil)
  2435. }
  2436. }
  2437. public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
  2438. if !isBlackCancelButton {
  2439. let cancelButtonAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
  2440. UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes , for: .normal)
  2441. }
  2442. guard let result = results.first else {
  2443. picker.dismiss(animated: true, completion: nil)
  2444. return
  2445. }
  2446. if result.itemProvider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
  2447. picker.dismiss(animated: true, completion: {
  2448. Nexilis.showLoader(text: "Preparing...".localized())
  2449. result.itemProvider.loadDataRepresentation(forTypeIdentifier: "com.compuserve.gif") { data, error in
  2450. if error != nil {
  2451. self.loadAnimatedMedia(from: result.itemProvider) { data, isGIF in
  2452. guard let data = data else {
  2453. print("Failed to load media")
  2454. return
  2455. }
  2456. DispatchQueue.main.async {
  2457. Nexilis.hideLoader() {
  2458. let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
  2459. if (self.textFieldSend.textColor != .lightGray) {
  2460. previewImageVC.currentTextTextField = self.textFieldSend.text
  2461. }
  2462. if isGIF {
  2463. previewImageVC.fromCopy = true
  2464. previewImageVC.isGIF = true
  2465. previewImageVC.dataGIF = data
  2466. previewImageVC.modalPresentationStyle = .custom
  2467. previewImageVC.delegate = self
  2468. previewImageVC.isAck = self.isAck
  2469. previewImageVC.isConfidential = self.isConfidential
  2470. } else {
  2471. let fileManager = FileManager.default
  2472. let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
  2473. let destinationURL = documentsDirectory.appendingPathComponent(UUID().uuidString + ".mov")
  2474. do {
  2475. try data.write(to: destinationURL)
  2476. previewImageVC.modalPresentationStyle = .custom
  2477. previewImageVC.urlVideoPhpPicker = destinationURL
  2478. previewImageVC.delegate = self
  2479. previewImageVC.isAck = self.isAck
  2480. previewImageVC.isConfidential = self.isConfidential
  2481. } catch {
  2482. }
  2483. }
  2484. self.present(previewImageVC, animated: true, completion: nil)
  2485. }
  2486. }
  2487. }
  2488. } else if let data = data {
  2489. DispatchQueue.main.async {
  2490. Nexilis.hideLoader() {
  2491. let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
  2492. if (self.textFieldSend.textColor != .lightGray) {
  2493. previewImageVC.currentTextTextField = self.textFieldSend.text
  2494. }
  2495. previewImageVC.fromCopy = true
  2496. previewImageVC.isGIF = true
  2497. previewImageVC.dataGIF = data
  2498. previewImageVC.modalPresentationStyle = .custom
  2499. previewImageVC.delegate = self
  2500. previewImageVC.isAck = self.isAck
  2501. previewImageVC.isConfidential = self.isConfidential
  2502. self.present(previewImageVC, animated: true, completion: nil)
  2503. }
  2504. }
  2505. }
  2506. }
  2507. })
  2508. } else if result.itemProvider.hasItemConformingToTypeIdentifier("public.image") {
  2509. picker.dismiss(animated: true, completion: {
  2510. Nexilis.showLoader(text: "Preparing...".localized())
  2511. result.itemProvider.loadDataRepresentation(forTypeIdentifier: "public.image") { data, error in
  2512. if let data = data {
  2513. do {
  2514. DispatchQueue.main.async {
  2515. Nexilis.hideLoader {
  2516. let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
  2517. if (self.textFieldSend.textColor != .lightGray) {
  2518. previewImageVC.currentTextTextField = self.textFieldSend.text
  2519. }
  2520. previewImageVC.fromCopy = true
  2521. previewImageVC.image = UIImage(data: data)
  2522. previewImageVC.modalPresentationStyle = .custom
  2523. previewImageVC.delegate = self
  2524. previewImageVC.isAck = self.isAck
  2525. previewImageVC.isConfidential = self.isConfidential
  2526. self.present(previewImageVC, animated: true, completion: nil)
  2527. }
  2528. }
  2529. } catch {
  2530. print("Error loading image data: \(error)")
  2531. }
  2532. } else {
  2533. print("Error: \(String(describing: error))")
  2534. }
  2535. }
  2536. })
  2537. } else if result.itemProvider.hasItemConformingToTypeIdentifier("public.movie") {
  2538. picker.dismiss(animated: true, completion: {
  2539. Nexilis.showLoader(text: "Preparing...".localized())
  2540. result.itemProvider.loadFileRepresentation(forTypeIdentifier: "public.movie") { url, error in
  2541. if let url = url {
  2542. let fileManager = FileManager.default
  2543. let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
  2544. var nameFile = url.lastPathComponent
  2545. if nameFile.contains("&uuid"){
  2546. nameFile = UUID().uuidString + ".mov"
  2547. }
  2548. let destinationURL = documentsDirectory.appendingPathComponent(nameFile)
  2549. do {
  2550. if fileManager.fileExists(atPath: destinationURL.path) {
  2551. try fileManager.removeItem(at: destinationURL)
  2552. }
  2553. try fileManager.copyItem(at: url, to: destinationURL)
  2554. DispatchQueue.main.async {
  2555. Nexilis.hideLoader {
  2556. let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
  2557. if (self.textFieldSend.textColor != .lightGray) {
  2558. previewImageVC.currentTextTextField = self.textFieldSend.text
  2559. }
  2560. previewImageVC.modalPresentationStyle = .custom
  2561. previewImageVC.urlVideoPhpPicker = destinationURL
  2562. previewImageVC.delegate = self
  2563. previewImageVC.isAck = self.isAck
  2564. previewImageVC.isConfidential = self.isConfidential
  2565. self.present(previewImageVC, animated: true, completion: nil)
  2566. }
  2567. }
  2568. } catch {
  2569. print("Error copying video file: \(error.localizedDescription)")
  2570. }
  2571. }
  2572. }
  2573. })
  2574. }
  2575. }
  2576. func loadAnimatedMedia(from provider: NSItemProvider, completion: @escaping (Data?, Bool) -> Void) {
  2577. // First: real GIF
  2578. if provider.hasItemConformingToTypeIdentifier("com.compuserve.gif") {
  2579. provider.loadFileRepresentation(forTypeIdentifier: "com.compuserve.gif") { url, error in
  2580. if let url = url, let data = try? Data(contentsOf: url) {
  2581. completion(data, true) // true = isGIF
  2582. } else {
  2583. // fallback
  2584. self.loadQuickTimeMovie(from: provider, completion: completion)
  2585. }
  2586. }
  2587. } else {
  2588. // fallback directly
  2589. self.loadQuickTimeMovie(from: provider, completion: completion)
  2590. }
  2591. }
  2592. private func loadQuickTimeMovie(from provider: NSItemProvider, completion: @escaping (Data?, Bool) -> Void) {
  2593. if provider.hasItemConformingToTypeIdentifier("com.apple.quicktime-movie") {
  2594. provider.loadFileRepresentation(forTypeIdentifier: "com.apple.quicktime-movie") { url, error in
  2595. if let url = url, let data = try? Data(contentsOf: url) {
  2596. completion(data, false) // false = it's MOV, not GIF
  2597. } else {
  2598. completion(nil, false)
  2599. }
  2600. }
  2601. } else {
  2602. completion(nil, false)
  2603. }
  2604. }
  2605. func sendChatFromPreviewImage(message_text: String, attachment_flag: String, image_id: String, video_id: String, thumb_id: String, gif_id: String, viewController: UIViewController, specFile: String) {
  2606. specFileString = specFile
  2607. sendChat(message_text: message_text, attachment_flag: attachment_flag, image_id: image_id, video_id: video_id, thumb_id: thumb_id, viewController: viewController, gif_id: gif_id)
  2608. }
  2609. }
  2610. extension EditorGroup: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPreviewControllerDataSource {
  2611. public func didSelectDocument(document: Any?) {
  2612. if (document != nil) {
  2613. self.previewItem = (document as! [URL])[0] as NSURL
  2614. let previewController = QLPreviewController()
  2615. previewController.dataSource = self
  2616. let vcHandleFile = UIViewController()
  2617. let nc = UINavigationController(rootViewController: vcHandleFile)
  2618. let attributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
  2619. let navBarAppearance = UINavigationBarAppearance()
  2620. nc.defaultStyle()
  2621. nc.modalPresentationStyle = .pageSheet
  2622. navBarAppearance.configureWithOpaqueBackground()
  2623. navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
  2624. navBarAppearance.titleTextAttributes = attributes
  2625. nc.navigationBar.standardAppearance = navBarAppearance
  2626. nc.navigationBar.scrollEdgeAppearance = navBarAppearance
  2627. let backButton = navigationQLPreviewDocument(title: "Cancel".localized(), style: .plain, target: self, action: #selector(cancelDocumentPreview))
  2628. vcHandleFile.navigationItem.leftBarButtonItem = backButton
  2629. let sendButton = navigationQLPreviewDocument(title: "Send".localized(), style: .done, target: self, action: #selector(sendDocument))
  2630. buttonSpec.setImage(UIImage(named: "pb_ic_attach_spc_off", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal).resize(target: CGSize(width: 30, height: 30)), for: .normal)
  2631. buttonSpec.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
  2632. buttonSpec.addTarget(self, action: #selector(showConfigurationFile), for: .touchUpInside)
  2633. let barButtonItemSpec = UIBarButtonItem(customView: buttonSpec)
  2634. vcHandleFile.navigationItem.rightBarButtonItems = [sendButton, barButtonItemSpec]
  2635. backButton.navigation = nc
  2636. sendButton.navigation = nc
  2637. if let viewVc = vcHandleFile.view {
  2638. vcHandleFile.title = self.previewItem?.lastPathComponent
  2639. vcHandleFile.addChild(previewController)
  2640. previewController.dataSource = self
  2641. previewController.view.frame = CGRect(x: 0, y: 0, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height)
  2642. previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  2643. viewVc.addSubview(previewController.view)
  2644. previewController.didMove(toParent: vcHandleFile)
  2645. self.present(nc, animated: true)
  2646. }
  2647. }
  2648. }
  2649. @objc private func showConfigurationFile() {
  2650. let modalVC = UIViewController()
  2651. if let viewModal = modalVC.view {
  2652. viewModal.backgroundColor = .whiteBubbleColor
  2653. let closeButton = UIButton(type: .close)
  2654. viewModal.addSubview(closeButton)
  2655. closeButton.anchor(top: viewModal.topAnchor, right: viewModal.rightAnchor, paddingTop: 15, paddingRight: 15, width: 30, height: 30)
  2656. closeButton.layer.cornerRadius = 15
  2657. closeButton.clipsToBounds = true
  2658. closeButton.backgroundColor = .lightGray.withAlphaComponent(0.1)
  2659. let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold)
  2660. closeButton.setImage(UIImage(systemName: "xmark", withConfiguration: config), for: .normal)
  2661. closeButton.addAction(UIAction { _ in
  2662. modalVC.dismiss(animated: true)
  2663. }, for: .touchUpInside)
  2664. let imageSpec = UIButton(type: .custom)
  2665. viewModal.addSubview(imageSpec)
  2666. imageSpec.anchor(top: viewModal.topAnchor, left: viewModal.leftAnchor, paddingTop: 25, paddingLeft: 15, width: 40, height: 40)
  2667. imageSpec.layer.cornerRadius = 20
  2668. imageSpec.clipsToBounds = true
  2669. imageSpec.backgroundColor = .lightGray.withAlphaComponent(0.1)
  2670. imageSpec.setImage(UIImage(named: "pb_ic_attach_spc", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal).resize(target: CGSize(width: 35, height: 35)), for: .normal)
  2671. let title = UILabel()
  2672. title.text = "Option for Attachment".localized()
  2673. viewModal.addSubview(title)
  2674. title.anchor(top: viewModal.topAnchor, left: imageSpec.rightAnchor, paddingTop: 23, paddingLeft: 10)
  2675. title.textColor = .label
  2676. title.font = .boldSystemFont(ofSize: 16)
  2677. let subtitle = UILabel()
  2678. subtitle.text = "Select option :".localized()
  2679. viewModal.addSubview(subtitle)
  2680. subtitle.anchor(top: title.bottomAnchor, left: imageSpec.rightAnchor, paddingLeft: 10)
  2681. subtitle.textColor = .gray
  2682. subtitle.font = .systemFont(ofSize: 14)
  2683. tableViewConfigFile = UITableView()
  2684. viewModal.addSubview(tableViewConfigFile)
  2685. tableViewConfigFile.backgroundColor = .white
  2686. tableViewConfigFile.layer.cornerRadius = 8.0
  2687. tableViewConfigFile.clipsToBounds = true
  2688. tableViewConfigFile.anchor(top: imageSpec.bottomAnchor, left: viewModal.leftAnchor, bottom: viewModal.bottomAnchor, right: viewModal.rightAnchor, paddingTop: 15, paddingLeft: 15, paddingBottom: 80, paddingRight: 15)
  2689. tableViewConfigFile.register(UITableViewCell.self, forCellReuseIdentifier: "cellConfigFile")
  2690. tableViewConfigFile.dataSource = self
  2691. tableViewConfigFile.delegate = self
  2692. tableViewConfigFile.separatorStyle = .singleLine
  2693. tableViewConfigFile.tableFooterView = UIView()
  2694. if #available(iOS 15.0, *) {
  2695. tableViewConfigFile.sectionHeaderTopPadding = 0
  2696. }
  2697. if #available(iOS 15.0, *) {
  2698. if let sheet = modalVC.sheetPresentationController {
  2699. sheet.detents = [.medium()]
  2700. }
  2701. } else {
  2702. // Fallback on earlier versions
  2703. }
  2704. }
  2705. UIApplication.shared.visibleViewController?.present(modalVC, animated: true)
  2706. }
  2707. @objc private func cancelDocumentPreview(sender: navigationQLPreviewDocument) {
  2708. sender.navigation.dismiss(animated: true, completion: nil)
  2709. }
  2710. @objc private func sendDocument(sender: navigationQLPreviewDocument) {
  2711. DispatchQueue.global().async {
  2712. if Nexilis.checkingAccess(key: "content_inspection") {
  2713. DispatchQueue.main.async {
  2714. Nexilis.showLoader(text: "Scanning File...".localized())
  2715. }
  2716. let result = (self.previewItem! as URL).validateFile()
  2717. DispatchQueue.main.async {
  2718. Nexilis.hideLoader {
  2719. sender.navigation.dismiss(animated: true, completion: {
  2720. if result == 1 {
  2721. sendIt()
  2722. } else {
  2723. APIS.showWarningFile(type: result)
  2724. }
  2725. })
  2726. }
  2727. }
  2728. } else {
  2729. DispatchQueue.main.async {
  2730. sendIt()
  2731. }
  2732. }
  2733. func sendIt() {
  2734. sender.navigation.dismiss(animated: true, completion: nil)
  2735. guard let previewItem = self.previewItem else { return }
  2736. guard var dataFile = try? Data(contentsOf: previewItem as URL) else { return }
  2737. func sanitizeFile(mimeType: String, sanitizeAction: (Data) -> MessageGuardLite.Result) -> Data? {
  2738. DispatchQueue.main.async {
  2739. Nexilis.showLoader(text: "Sanitizing your \(mimeType.contains("pdf") ? "pdf file" : "image") (Message Guard)".localized())
  2740. }
  2741. let res = sanitizeAction(dataFile)
  2742. defer {
  2743. DispatchQueue.main.async { Nexilis.hideLoader {} }
  2744. }
  2745. if res.verdict == .block {
  2746. DispatchQueue.main.async {
  2747. Nexilis.hideLoader {
  2748. APIS.showMessageGuardFile(mime: res.mime)
  2749. }
  2750. }
  2751. return nil
  2752. }
  2753. return res.data ?? Data()
  2754. }
  2755. func processIt(with data: Data) {
  2756. guard let urlFile = self.previewItem?.absoluteString else { return }
  2757. let originalFileName = (urlFile as NSString).lastPathComponent.removingPercentEncoding ?? "file"
  2758. let renamedNameFile = "Nexilis_\(Date().currentTimeMillis())_\(originalFileName)"
  2759. let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
  2760. let fileURL = documentsDirectory.appendingPathComponent(renamedNameFile)
  2761. if !FileManager.default.fileExists(atPath: fileURL.path) {
  2762. try? data.write(to: fileURL)
  2763. }
  2764. DispatchQueue.main.async {
  2765. self.sendChat(
  2766. message_text: "\(originalFileName)|",
  2767. attachment_flag: "6",
  2768. file_id: renamedNameFile,
  2769. viewController: self
  2770. )
  2771. }
  2772. }
  2773. if Nexilis.checkingAccess(key: "message_guard") {
  2774. DispatchQueue.global().async {
  2775. let guardLite = MessageGuardLite(limits: .defaults())
  2776. let mimeType = MessageGuardLite.sniffMime(dataFile)
  2777. if mimeType == "image/png" || mimeType == "image/jpeg" {
  2778. if let sanitized = sanitizeFile(mimeType: mimeType, sanitizeAction: guardLite.sanitizeImage) {
  2779. dataFile = sanitized
  2780. } else { return }
  2781. } else if mimeType == "application/pdf" {
  2782. if let sanitized = sanitizeFile(mimeType: mimeType, sanitizeAction: guardLite.sanitizePdf) {
  2783. dataFile = sanitized
  2784. } else { return }
  2785. }
  2786. processIt(with: dataFile)
  2787. }
  2788. } else {
  2789. processIt(with: dataFile)
  2790. }
  2791. }
  2792. }
  2793. }
  2794. }
  2795. extension EditorGroup: UITextViewDelegate, CustomTextViewPasteDelegate {
  2796. func customTextViewDidPasteText(image: UIImage?, dataGIF: Data?) {
  2797. let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
  2798. previewImageVC.image = image
  2799. previewImageVC.isGIF = image == nil
  2800. previewImageVC.fromCopy = true
  2801. previewImageVC.dataGIF = dataGIF
  2802. previewImageVC.currentTextTextField = textFieldSend.text
  2803. previewImageVC.modalPresentationStyle = .custom
  2804. previewImageVC.delegate = self
  2805. previewImageVC.isAck = self.isAck
  2806. previewImageVC.isConfidential = self.isConfidential
  2807. self.present(previewImageVC, animated: true, completion: nil)
  2808. }
  2809. public func textViewDidChangeSelection(_ textView: UITextView) {
  2810. lastPositionCursorMention = textView.selectedRange.location
  2811. var isShowMention = false
  2812. let fulltextForMention = textView.text.prefix(lastPositionCursorMention)
  2813. let lines = fulltextForMention.split(separator: "\n")
  2814. if let lastLineIndex = lines.lastIndex(where: { !$0.isEmpty }) {
  2815. let words = lines[lastLineIndex].split(separator: " ")
  2816. if let lastWordIndex = words.lastIndex(where: { !$0.isEmpty }) {
  2817. let mentionText = words[lastWordIndex]
  2818. let lastChar = fulltextForMention.last
  2819. if lastChar != "\n" && lastChar != " " {
  2820. if mentionText.starts(with: "@") || (mentionText.count >= 2 && (self.textFieldSend.textColor != UIColor.lightGray || heightTableEditMention != nil) && extractFromAtIfSymbolsBefore(String(mentionText)) == nil) {
  2821. showMention(text: mentionText.starts(with: "@") ? String(mentionText.dropFirst()) : String(mentionText))
  2822. isShowMention = true
  2823. } else if let textM = extractFromAtIfSymbolsBefore(String(mentionText)) {
  2824. showMention(text: String(textM.dropFirst()))
  2825. isShowMention = true
  2826. }
  2827. }
  2828. }
  2829. }
  2830. if !isShowMention {
  2831. hideMention()
  2832. }
  2833. if var nowTextFieldSend = self.textFieldSend {
  2834. if isEditingMessage {
  2835. nowTextFieldSend = editTextView
  2836. }
  2837. if let sr = nowTextFieldSend.selectedTextRange {
  2838. if let fnt = nowTextFieldSend.font {
  2839. let cursorPosition = textView.caretRect(for: sr.start).origin
  2840. let doubleCurrentLine = cursorPosition.y / fnt.lineHeight
  2841. if doubleCurrentLine.isFinite {
  2842. let currentLine = Int(ceil(doubleCurrentLine))
  2843. UIView.animate(withDuration: 0.3) {
  2844. let layoutManager = textView.layoutManager
  2845. var numberOfLines = 0
  2846. var index = 0
  2847. let numberOfGlyphs = layoutManager.numberOfGlyphs
  2848. while index < numberOfGlyphs {
  2849. var lineRange = NSRange()
  2850. layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange)
  2851. index = NSMaxRange(lineRange)
  2852. numberOfLines += 1
  2853. }
  2854. if currentLine == 1 && (numberOfLines == 1 || numberOfLines == 0) {
  2855. if self.isEditingMessage {
  2856. self.constraintHeighteditTextView.constant = 40
  2857. } else {
  2858. self.heightTextFieldSend.constant = 40
  2859. }
  2860. } else if (self.heightTextFieldSend.constant < 95.0 || (self.constraintHeighteditTextView != nil && self.constraintHeighteditTextView.constant < 95.0)) && currentLine >= 4 {
  2861. if self.isEditingMessage {
  2862. self.constraintHeighteditTextView.constant = 95.0
  2863. } else {
  2864. self.heightTextFieldSend.constant = 95.0
  2865. }
  2866. } else if currentLine < 4 && numberOfLines < 5 {
  2867. if (nowTextFieldSend.text.count > 0 && self.heightTextFieldSend.constant != nowTextFieldSend.contentSize.height) {
  2868. if self.isEditingMessage {
  2869. self.constraintHeighteditTextView.constant = nowTextFieldSend.contentSize.height
  2870. } else {
  2871. self.heightTextFieldSend.constant = nowTextFieldSend.contentSize.height
  2872. }
  2873. }
  2874. }
  2875. }
  2876. }
  2877. }
  2878. }
  2879. }
  2880. if self.isEditingMessage && textView == editTextView {
  2881. if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  2882. buttonSendEdit.isEnabled = false
  2883. } else if !buttonSendEdit.isEnabled {
  2884. buttonSendEdit.isEnabled = true
  2885. }
  2886. }
  2887. //indention code:
  2888. let text = textView.text ?? ""
  2889. let cursorPositionIndent = textView.selectedRange.location
  2890. // Prevent moving cursor before the 2-space indent
  2891. var adjustedCursorPosition = cursorPositionIndent
  2892. for line in lines {
  2893. if let range = text.range(of: line), NSRange(range, in: text).contains(cursorPositionIndent) {
  2894. if line.hasPrefix(" •") || line.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
  2895. let startOfLine = text.distance(from: text.startIndex, to: range.lowerBound)
  2896. let minCursorPosition = startOfLine + 2 // Prevent cursor before indentation
  2897. if cursorPositionIndent < minCursorPosition {
  2898. adjustedCursorPosition = minCursorPosition
  2899. }
  2900. }
  2901. break
  2902. }
  2903. }
  2904. if adjustedCursorPosition != cursorPositionIndent {
  2905. textView.selectedRange = NSRange(location: adjustedCursorPosition, length: 0)
  2906. }
  2907. }
  2908. func extractFromAtIfSymbolsBefore(_ text: String) -> String? {
  2909. guard let atIndex = text.firstIndex(of: "@") else {
  2910. return nil
  2911. }
  2912. let beforeAt = text[..<atIndex]
  2913. let afterAt = text[atIndex...]
  2914. // Define symbols as anything that's not a letter or digit
  2915. let symbolSet = CharacterSet.letters.union(.decimalDigits).inverted
  2916. let isAllSymbols = beforeAt.unicodeScalars.allSatisfy { symbolSet.contains($0) }
  2917. return isAllSymbols ? String(afterAt) : nil
  2918. }
  2919. public func textViewDidChange(_ textView: UITextView) {
  2920. if textView.text.count == 0 {
  2921. isAlwaysHideLinkPreview = false
  2922. }
  2923. if allowTyping {
  2924. allowTyping = false
  2925. if dataTopic["chat_id"] as? String ?? "" == "" {
  2926. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [dataGroup["group_id"] as? String ?? ""])
  2927. sendTyping(l_pin: dataGroup["group_id"] as? String ?? "")
  2928. } else {
  2929. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [dataTopic["chat_id"] as? String ?? ""])
  2930. sendTyping(l_pin: dataTopic["chat_id"] as? String ?? "")
  2931. }
  2932. DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: {
  2933. self.allowTyping = true
  2934. })
  2935. }
  2936. timerCheckLink?.invalidate()
  2937. timerCheckLink = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {_ in
  2938. self.checkLink(fullText: textView.text)
  2939. })
  2940. //indention code:
  2941. let text = textView.text ?? ""
  2942. let cursorPosition = textView.selectedRange.location
  2943. let tempListMention = listMentionInTextField
  2944. if listMentionInTextField.count > 0 {
  2945. for j in 0..<listMentionInTextField.count {
  2946. var index = j
  2947. if tempListMention.count != listMentionInTextField.count {
  2948. index = j - (tempListMention.count - listMentionInTextField.count)
  2949. }
  2950. var upper = (Int(listMentionInTextField[index].ex_block ?? "0") ?? 0)
  2951. if cursorPosition <= upper {
  2952. upper += text.count - lastTextLength
  2953. listMentionInTextField[index].ex_block = "\(upper)"
  2954. }
  2955. let lower = upper - listMentionInTextField[index].fullName.count
  2956. let name = listMentionInTextField[index].fullName.trimmingCharacters(in: .whitespaces)
  2957. if textView.text.substring(from: lower, to: upper) != "@\(name)" {
  2958. listMentionInTextField.remove(at: index)
  2959. }
  2960. }
  2961. }
  2962. // Handle Bullets (- [space] + letter → • )
  2963. let bulletPattern = #"(?<=\n|^)- (\S)"#
  2964. if let match = text.range(of: bulletPattern, options: .regularExpression) {
  2965. let matchedText = text[match]
  2966. if let spaceIndex = matchedText.firstIndex(of: " ") {
  2967. let firstLetter = matchedText[matchedText.index(after: spaceIndex)...]
  2968. let replacedText = text.replacingOccurrences(of: matchedText, with: " • \(firstLetter)", range: match)
  2969. let newCursorPosition = cursorPosition + 2 // Adjust cursor position
  2970. textView.text = replacedText
  2971. textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
  2972. }
  2973. }
  2974. // Handle Numbered Lists (e.g., "1. " [space] + letter → " 1.")
  2975. let numberPattern = #"(?<=\n|^)(\d+)\. (\S)"# // Matches "1. X"
  2976. if let match = text.range(of: numberPattern, options: .regularExpression) {
  2977. let matchedText = text[match]
  2978. if let spaceIndex = matchedText.firstIndex(of: " ") {
  2979. let firstLetter = matchedText[matchedText.index(after: spaceIndex)...]
  2980. let replacedText = text.replacingOccurrences(of: matchedText, with: " \(matchedText)", range: match)
  2981. let newCursorPosition = cursorPosition + 2 // Adjust cursor
  2982. textView.text = replacedText
  2983. textView.selectedRange = NSRange(location: newCursorPosition, length: 0)
  2984. }
  2985. }
  2986. handleRichText(textView)
  2987. lastTextLength = text.count
  2988. }
  2989. private func showMention(text: String) {
  2990. if self.contraintBottomMention.constant < 0 {
  2991. if !isEditingMessage {
  2992. self.contraintBottomMention.constant = 25 + constraintBottomAttachment.constant + self.heightTextFieldSend.constant + self.viewTextfield.bounds.height
  2993. UIView.animate(withDuration: 0.5, animations: {
  2994. self.view.layoutIfNeeded()
  2995. })
  2996. }
  2997. }
  2998. listMentionWithText.removeAll()
  2999. Database.shared.database?.inTransaction({ fmdb, rollback in
  3000. do {
  3001. let idMe = User.getMyPin()!
  3002. if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name || ' ' || ifnull(last_name, '') name FROM GROUPZ_MEMBER where group_id='\(self.dataGroup["group_id"] as? String ?? "")' AND f_pin <> '\(idMe)' AND name LIKE '%\(text)%'") {
  3003. while cursor.next() {
  3004. let user = User(pin: "")
  3005. user.pin = cursor.string(forColumnIndex: 0) ?? ""
  3006. user.firstName = cursor.string(forColumnIndex: 1) ?? ""
  3007. if !user.pin.isEmpty {
  3008. let userFromBuddy = User.getDataCanNil(pin: user.pin, fmdb: fmdb)
  3009. if userFromBuddy != nil {
  3010. listMentionWithText.append(userFromBuddy!)
  3011. } else {
  3012. listMentionWithText.append(user)
  3013. }
  3014. }
  3015. }
  3016. cursor.close()
  3017. }
  3018. listMentionWithText.removeAll(where: { listMentionInTextField.contains($0) })
  3019. var nowTableMention = tableMention!
  3020. var nowHeightTableMention = heightTableMention!
  3021. if isEditingMessage {
  3022. nowTableMention = tableMentionEdit
  3023. if heightTableEditMention != nil {
  3024. nowHeightTableMention = heightTableEditMention
  3025. } else {
  3026. return
  3027. }
  3028. }
  3029. if listMentionWithText.count > 0 {
  3030. if listMentionWithText.count < 5 {
  3031. nowHeightTableMention.constant = CGFloat(44 * listMentionWithText.count)
  3032. } else {
  3033. nowHeightTableMention.constant = 44 * 4
  3034. }
  3035. nowTableMention.reloadData()
  3036. } else {
  3037. nowHeightTableMention.constant = 44
  3038. self.hideMention()
  3039. }
  3040. } catch {
  3041. rollback.pointee = true
  3042. print("Access database error: \(error.localizedDescription)")
  3043. }
  3044. })
  3045. }
  3046. private func hideMention() {
  3047. if self.contraintBottomMention.constant > 0 {
  3048. listMentionWithText.removeAll()
  3049. tableMention.reloadData()
  3050. self.contraintBottomMention.constant = 0 - self.heightTableMention.constant
  3051. UIView.animate(withDuration: 0.5, animations: {
  3052. self.view.layoutIfNeeded()
  3053. })
  3054. } else if self.heightTableEditMention != nil && self.heightTableEditMention.constant != 0 {
  3055. listMentionWithText.removeAll()
  3056. tableMentionEdit.reloadData()
  3057. self.heightTableEditMention.constant = 0
  3058. }
  3059. }
  3060. private func checkLink(fullText: String) {
  3061. if !isAlwaysHideLinkPreview {
  3062. var text = ""
  3063. let listTextSplitBreak = fullText.components(separatedBy: "\n")
  3064. let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
  3065. if indexFirstLinkSplitBreak != nil {
  3066. let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
  3067. let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
  3068. if indexFirstLinkSplitSpace != nil {
  3069. text = listTextSplitSpace[indexFirstLinkSplitSpace!]
  3070. }
  3071. }
  3072. if !text.isEmpty {
  3073. var stringURl = text
  3074. if stringURl.starts(with: "www.") {
  3075. stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
  3076. }
  3077. var dataURL = ""
  3078. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3079. do {
  3080. if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'") {
  3081. while cursor.next() {
  3082. if let data = cursor.string(forColumnIndex: 0) {
  3083. dataURL = data
  3084. }
  3085. }
  3086. cursor.close()
  3087. }
  3088. } catch {
  3089. rollback.pointee = true
  3090. print("Access database error: \(error.localizedDescription)")
  3091. }
  3092. })
  3093. if !dataURL.isEmpty {
  3094. if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  3095. let imageUrl = data["imageUrl"] as? String
  3096. let link = data["link"] as? String ?? ""
  3097. if imageUrl == nil || (link.contains("youtube.com") && link.contains("watch?v=") && !imageUrl!.contains("img.youtube.com/vi/")) {
  3098. dataURL = ""
  3099. }
  3100. }
  3101. }
  3102. if !dataURL.isEmpty {
  3103. if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  3104. let title = data["title"] as? String ?? ""
  3105. let description = data["description"] as? String ?? ""
  3106. let imageUrl = data["imageUrl"] as? String
  3107. if self.showingLink != text {
  3108. self.showingLink = text
  3109. self.deleteLinkPreview()
  3110. if !textFieldSend.text.isEmpty || textFieldSend.text.contains(text){
  3111. self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: text)
  3112. }
  3113. }
  3114. }
  3115. } else {
  3116. let urlConfig = URLSessionConfiguration.default
  3117. let sessionDelegate = SelfSignedURLSessionDelegate()
  3118. let session = URLSession(configuration: urlConfig, delegate: sessionDelegate, delegateQueue: nil)
  3119. let slp = SwiftLinkPreview(session: session,
  3120. workQueue: SwiftLinkPreview.defaultWorkQueue,
  3121. responseQueue: DispatchQueue.main,
  3122. cache: DisabledCache.instance)
  3123. let preview = slp.preview(stringURl,
  3124. onSuccess: { result in
  3125. let title = result.title?.trimmingCharacters(in: .whitespacesAndNewlines)
  3126. .nilIfEmpty ?? URL(string: text)?.host ?? "Untitled"
  3127. let description: String
  3128. if text.contains("google.com") {
  3129. description = "" // special rule for google
  3130. } else {
  3131. description = result.description?.trimmingCharacters(in: .whitespacesAndNewlines)
  3132. .nilIfEmpty ?? ""
  3133. }
  3134. let imageUrl = self.youtubeThumbnail(from: text)
  3135. ?? result.image
  3136. ?? result.icon
  3137. ?? ""
  3138. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3139. do {
  3140. var dataJson: [String: Any] = [:]
  3141. dataJson["title"] = title
  3142. dataJson["description"] = description
  3143. dataJson["imageUrl"] = imageUrl
  3144. dataJson["link"] = text
  3145. guard let json = String(data: try! JSONSerialization.data(withJSONObject: dataJson, options: []), encoding: String.Encoding.utf8) else {
  3146. return
  3147. }
  3148. _ = try Database.shared.insertRecord(fmdb: fmdb, table: "LINK_PREVIEW", cvalues: [
  3149. "id" : "\(Date().currentTimeMillis().toHex())",
  3150. "link" : text,
  3151. "data_link" : json,
  3152. "retry": 0
  3153. ], replace: true)
  3154. } catch {
  3155. rollback.pointee = true
  3156. print("Access database error: \(error.localizedDescription)")
  3157. }
  3158. })
  3159. if self.showingLink != text {
  3160. self.showingLink = text
  3161. self.deleteLinkPreview()
  3162. if !self.textFieldSend.text.isEmpty || self.textFieldSend.text.contains(text){
  3163. self.buildPreviewLink(imageUrl: imageUrl, title: title, description: description, stringURl: text)
  3164. }
  3165. }
  3166. },
  3167. onError: { error in
  3168. self.deleteLinkPreview()
  3169. })
  3170. }
  3171. } else {
  3172. deleteLinkPreview()
  3173. }
  3174. }
  3175. }
  3176. private func buildPreviewLink(imageUrl: String?, title: String, description: String?, stringURl: String) {
  3177. if !self.viewTextfield.subviews.contains(self.containerLink){
  3178. UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
  3179. self.constraintTopTextField.constant = self.constraintTopTextField.constant + 80
  3180. if self.contraintBottomMention.constant > 0 {
  3181. self.contraintBottomMention.constant = self.contraintBottomMention.constant + 80 + self.heightTextFieldSend.constant
  3182. }
  3183. }, completion: nil)
  3184. }
  3185. self.viewTextfield.addSubview(self.containerLink)
  3186. self.containerLink.translatesAutoresizingMaskIntoConstraints = false
  3187. self.containerLink.leadingAnchor.constraint(equalTo: self.viewTextfield.leadingAnchor).isActive = true
  3188. self.containerLink.bottomAnchor.constraint(equalTo: self.textFieldSend.topAnchor).isActive = true
  3189. self.containerLink.trailingAnchor.constraint(equalTo: self.viewTextfield.trailingAnchor).isActive = true
  3190. self.containerLink.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
  3191. self.containerLink.backgroundColor = .secondaryColor
  3192. if self.reffId != nil {
  3193. self.bottomAnchorPreviewReply.isActive = false
  3194. self.bottomAnchorPreviewReply = self.containerPreviewReply.bottomAnchor.constraint(equalTo: self.containerLink.topAnchor)
  3195. self.bottomAnchorPreviewReply.isActive = true
  3196. }
  3197. let imagePreview = UIImageView()
  3198. if imageUrl != nil {
  3199. self.containerLink.addSubview(imagePreview)
  3200. imagePreview.translatesAutoresizingMaskIntoConstraints = false
  3201. imagePreview.leadingAnchor.constraint(equalTo: self.containerLink.leadingAnchor).isActive = true
  3202. imagePreview.bottomAnchor.constraint(equalTo: self.containerLink.bottomAnchor).isActive = true
  3203. imagePreview.topAnchor.constraint(equalTo: self.containerLink.topAnchor).isActive = true
  3204. imagePreview.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
  3205. imagePreview.loadImageAsync(with: imageUrl)
  3206. imagePreview.contentMode = .scaleAspectFit
  3207. }
  3208. let titlePreview = UILabel()
  3209. self.containerLink.addSubview(titlePreview)
  3210. titlePreview.translatesAutoresizingMaskIntoConstraints = false
  3211. if imageUrl != nil {
  3212. titlePreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
  3213. } else {
  3214. titlePreview.leadingAnchor.constraint(equalTo: self.containerLink.leadingAnchor, constant: 5.0).isActive = true
  3215. }
  3216. titlePreview.topAnchor.constraint(equalTo: self.containerLink.topAnchor, constant: 25.0).isActive = true
  3217. titlePreview.trailingAnchor.constraint(equalTo: self.containerLink.trailingAnchor, constant: -80.0).isActive = true
  3218. titlePreview.text = title
  3219. titlePreview.font = UIFont.systemFont(ofSize: 14.0, weight: .bold)
  3220. titlePreview.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  3221. let descPreview = UILabel()
  3222. self.containerLink.addSubview(descPreview)
  3223. descPreview.translatesAutoresizingMaskIntoConstraints = false
  3224. if imageUrl != nil {
  3225. descPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
  3226. } else {
  3227. descPreview.leadingAnchor.constraint(equalTo: self.containerLink.leadingAnchor, constant: 5.0).isActive = true
  3228. }
  3229. descPreview.topAnchor.constraint(equalTo: titlePreview.bottomAnchor).isActive = true
  3230. descPreview.trailingAnchor.constraint(equalTo: self.containerLink.trailingAnchor, constant: -80.0).isActive = true
  3231. descPreview.text = description
  3232. descPreview.font = UIFont.systemFont(ofSize: 12.0 + offset())
  3233. descPreview.textColor = .gray
  3234. descPreview.numberOfLines = 1
  3235. let linkPreview = UILabel()
  3236. self.containerLink.addSubview(linkPreview)
  3237. linkPreview.translatesAutoresizingMaskIntoConstraints = false
  3238. if imageUrl != nil {
  3239. linkPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
  3240. } else {
  3241. linkPreview.leadingAnchor.constraint(equalTo: self.containerLink.leadingAnchor, constant: 5.0).isActive = true
  3242. }
  3243. linkPreview.topAnchor.constraint(equalTo: descPreview.bottomAnchor).isActive = true
  3244. linkPreview.trailingAnchor.constraint(equalTo: self.containerLink.trailingAnchor, constant: -80.0).isActive = true
  3245. linkPreview.text = stringURl
  3246. linkPreview.font = UIFont.systemFont(ofSize: 10.0 + offset())
  3247. linkPreview.textColor = .gray
  3248. linkPreview.numberOfLines = 1
  3249. let cancelPreview = UIButton(type: .custom)
  3250. self.containerLink.addSubview(cancelPreview)
  3251. cancelPreview.translatesAutoresizingMaskIntoConstraints = false
  3252. cancelPreview.trailingAnchor.constraint(equalTo: self.containerLink.trailingAnchor, constant: -10).isActive = true
  3253. cancelPreview.centerYAnchor.constraint(equalTo: self.containerLink.centerYAnchor).isActive = true
  3254. cancelPreview.setImage(UIImage(systemName: "xmark.circle" , withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular, scale: .default)), for: .normal)
  3255. cancelPreview.addTarget(nil, action: #selector(self.removeLinkPreviewUntilEmptyTextView), for: .touchUpInside)
  3256. cancelPreview.backgroundColor = .clear
  3257. cancelPreview.tintColor = .mainColor
  3258. }
  3259. public func textViewDidBeginEditing(_ textView: UITextView) {
  3260. if textView.textColor == UIColor.lightGray {
  3261. textView.text = nil
  3262. textView.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black
  3263. }
  3264. }
  3265. public func textViewDidEndEditing(_ textView: UITextView) {
  3266. if textView.text.isEmpty && textView != editTextView {
  3267. textView.textColor = UIColor.lightGray
  3268. textView.text = "Send message".localized()
  3269. }
  3270. }
  3271. public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  3272. if text.isEmpty {
  3273. if listMentionInTextField.count > 0 {
  3274. for i in 0..<listMentionInTextField.count {
  3275. if lastPositionCursorMention == Int(listMentionInTextField[i].ex_block!)! + 1 {
  3276. let fulltextForMention = textView.text.substring(from: 0, to: lastPositionCursorMention - 1)
  3277. let diff = textView.text.count - fulltextForMention.count
  3278. var text = textView.text ?? ""
  3279. let nameMention = listMentionInTextField[i].fullName.trimmingCharacters(in: .whitespaces)
  3280. let rangeReplacement = NSRange(location: lastPositionCursorMention - nameMention.count - 1, length: nameMention.count + 1)
  3281. let replacementText = ""
  3282. let copyAttributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "", listMentionInTextField: listMentionInTextField)
  3283. copyAttributedText.removeAttribute(.foregroundColor, range: rangeReplacement)
  3284. textView.attributedText = copyAttributedText
  3285. // Replace the old text with the new text using the replaceSubrange(_:with:) method
  3286. if let startIndex = text.index(text.startIndex, offsetBy: rangeReplacement.location, limitedBy: text.endIndex),
  3287. let endIndex = text.index(startIndex, offsetBy: rangeReplacement.length, limitedBy: text.endIndex) {
  3288. text.replaceSubrange(startIndex..<endIndex, with: replacementText)
  3289. }
  3290. listMentionInTextField.remove(at: i)
  3291. textView.attributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "", listMentionInTextField: listMentionInTextField)
  3292. let newPosition = textView.position(from: textView.beginningOfDocument, offset: textView.text.count - diff)
  3293. textView.selectedTextRange = textView.textRange(from: newPosition!, to: newPosition!)
  3294. textViewDidChangeSelection(textView)
  3295. handleRichText(textView)
  3296. return false
  3297. }
  3298. }
  3299. }
  3300. }
  3301. let indent = handleIndent(textView, range, text)
  3302. if !indent {
  3303. textViewDidChangeSelection(textView)
  3304. handleRichText(textView)
  3305. return indent
  3306. }
  3307. if (textView.text.count == 0) {
  3308. return text != "\n"
  3309. }
  3310. return true
  3311. }
  3312. private func handleIndent(_ textView: UITextView, _ range: NSRange, _ text: String) -> Bool {
  3313. guard let nsText = textView.text as NSString? else { return true }
  3314. let newText = nsText.replacingCharacters(in: range, with: text)
  3315. var lines = newText.components(separatedBy: "\n")
  3316. // Ensure range location is valid, considering Unicode scalars
  3317. guard let textRange = Range(range, in: textView.text) else { return true }
  3318. let prefixText = textView.text[..<textRange.lowerBound]
  3319. let affectedLineIndex = prefixText.components(separatedBy: "\n").count - 1
  3320. guard affectedLineIndex >= 0, affectedLineIndex < lines.count else { return true }
  3321. let affectedLine = lines[affectedLineIndex]
  3322. // Prevent deleting two-space indentation before bullet/number
  3323. if affectedLine.hasPrefix(" •") || affectedLine.range(of: #"^\s{2}\d+\."#, options: .regularExpression) != nil {
  3324. if let lineStart = textView.text.range(of: affectedLine)?.lowerBound,
  3325. let startIndex = textView.text.distance(of: lineStart) {
  3326. if range.location == startIndex || range.location == startIndex + 1 {
  3327. return false
  3328. }
  3329. }
  3330. }
  3331. // Auto-indent new lines based on previous line
  3332. if text == "\n" {
  3333. let previousLine = lines[affectedLineIndex]
  3334. if previousLine.hasPrefix(" •") {
  3335. let newBullet = "\n • "
  3336. textView.text = nsText.replacingCharacters(in: range, with: newBullet)
  3337. textView.selectedRange = NSRange(location: range.location + newBullet.utf16.count, length: 0)
  3338. return false
  3339. }
  3340. if let match = previousLine.range(of: #"^\s{2}(\d+)\."#, options: .regularExpression),
  3341. let numberMatch = previousLine[match].components(separatedBy: ".").first,
  3342. let number = Int(numberMatch.trimmingCharacters(in: .whitespaces)) {
  3343. let newNumber = "\n \(number + 1). "
  3344. textView.text = nsText.replacingCharacters(in: range, with: newNumber)
  3345. textView.selectedRange = NSRange(location: range.location + newNumber.utf16.count, length: 0)
  3346. return false
  3347. }
  3348. }
  3349. // Handle Backspace on Empty Bullet (Convert " • " → "- ")
  3350. if text.isEmpty && affectedLine.trimmingCharacters(in: .whitespaces) == "•" {
  3351. lines[affectedLineIndex] = "- " // Replace " • " with "- "
  3352. textView.text = lines.joined(separator: "\n")
  3353. textView.selectedRange = NSRange(location: range.location - 1, length: 0)
  3354. return false
  3355. }
  3356. // Handle Backspace on Numbered List
  3357. if text.isEmpty, affectedLine.range(of: #"^\s{2}(\d+)\.$"#, options: .regularExpression) != nil {
  3358. lines[affectedLineIndex] = affectedLine.trimmingCharacters(in: .whitespaces)
  3359. textView.text = lines.joined(separator: "\n")
  3360. textView.selectedRange = NSRange(location: range.location - 1, length: 0)
  3361. return false
  3362. }
  3363. return true
  3364. }
  3365. private func handleRichText(_ textView: UITextView) {
  3366. textView.preserveCursorPosition(withChanges: { _ in
  3367. textView.attributedText = textView.text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "", listMentionInTextField: self.listMentionInTextField)
  3368. return .preserveCursor
  3369. })
  3370. }
  3371. public func textView(_ textView: UITextView, shouldInteractWith URL: URL?, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
  3372. var urlString: String?
  3373. if let url = URL {
  3374. urlString = url.absoluteString
  3375. } else {
  3376. if let range = Range(characterRange, in: textView.text) {
  3377. let tappedText = String(textView.text[range])
  3378. urlString = tappedText
  3379. }
  3380. }
  3381. guard let finalURL = urlString else {
  3382. return false
  3383. }
  3384. switch interaction {
  3385. case .invokeDefaultAction:
  3386. let gesture = ObjectGesture()
  3387. gesture.message_id = finalURL
  3388. tapMessageText(gesture)
  3389. return false
  3390. case .presentActions:
  3391. UIPasteboard.general.string = finalURL
  3392. self.view.makeToast("Link Copied".localized(), duration: 3)
  3393. return false
  3394. case .preview:
  3395. return true
  3396. @unknown default:
  3397. return true
  3398. }
  3399. }
  3400. }
  3401. extension EditorGroup: UIContextMenuInteractionDelegate {
  3402. public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
  3403. if showMenuContext {
  3404. showMenuContext = false
  3405. interaction.view!.removeInteraction(interaction)
  3406. }
  3407. }
  3408. public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
  3409. if textFieldSend.isFirstResponder {
  3410. textFieldSend.resignFirstResponder()
  3411. }
  3412. let indexPath = self.tableChatView.indexPathForRow(at: interaction.view!.convert(location, to: self.tableChatView))
  3413. let dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexPath!.section]})
  3414. var star: UIAction
  3415. if (dataMessages[indexPath!.row]["is_stared"] as? String ?? "" == "0") {
  3416. star = UIAction(title: "Star".localized(), image: UIImage(systemName: "star"), handler: {(_) in
  3417. if self.removed {
  3418. return
  3419. }
  3420. DispatchQueue.global().async {
  3421. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3422. do {
  3423. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  3424. "is_stared" : 1
  3425. ], _where: "message_id = '\(dataMessages[indexPath!.row]["message_id"] as? String ?? "")'")
  3426. } catch {
  3427. rollback.pointee = true
  3428. print("Access database error: \(error.localizedDescription)")
  3429. }
  3430. })
  3431. }
  3432. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == dataMessages[indexPath!.row]["message_id"] as? String ?? ""})
  3433. if idx != nil{
  3434. self.dataMessages[idx!]["is_stared"] = "1"
  3435. }
  3436. self.tableChatView.reloadRows(at: [indexPath!], with: .none)
  3437. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "listenerStarMessage"), object: nil, userInfo: nil)
  3438. })
  3439. } else {
  3440. star = UIAction(title: "Unstar".localized(), image: UIImage(systemName: "star.slash"), handler: {(_) in
  3441. if self.removed {
  3442. return
  3443. }
  3444. DispatchQueue.global().async {
  3445. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3446. do {
  3447. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  3448. "is_stared" : 0
  3449. ], _where: "message_id = '\(dataMessages[indexPath!.row]["message_id"] as? String ?? "")'")
  3450. } catch {
  3451. rollback.pointee = true
  3452. print("Access database error: \(error.localizedDescription)")
  3453. }
  3454. })
  3455. }
  3456. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == dataMessages[indexPath!.row]["message_id"] as? String ?? ""})
  3457. if idx != nil{
  3458. self.dataMessages[idx!]["is_stared"] = "0"
  3459. }
  3460. self.tableChatView.reloadRows(at: [indexPath!], with: .none)
  3461. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "listenerStarMessage"), object: nil, userInfo: nil)
  3462. })
  3463. }
  3464. let reply = UIAction(title: "Reply".localized(), image: UIImage(systemName: "arrowshape.turn.up.left"), handler: {(_) in
  3465. if self.removed {
  3466. return
  3467. }
  3468. if self.isSearching {
  3469. self.cancelAction()
  3470. }
  3471. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
  3472. self.handleReply(indexPath: indexPath!)
  3473. })
  3474. })
  3475. var pin: UIAction
  3476. if (dataMessages[indexPath!.row][TypeDataMessage.is_pinned] as? String ?? "0" == "0") {
  3477. pin = UIAction(title: "Pin".localized(), image: UIImage(systemName: "pin"), handler: {(_) in
  3478. if self.removed {
  3479. return
  3480. }
  3481. if self.isSearching {
  3482. self.cancelAction()
  3483. }
  3484. var checkDataPinned = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  3485. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
  3486. if checkDataPinned.count == 3 {
  3487. let alert = UIAlertController(title: "Replace oldest pin?".localized(),
  3488. message: "Your pin will replace the oldest one.".localized(),
  3489. preferredStyle: .alert)
  3490. alert.addAction(UIAlertAction(title: "Continue", style: .default) { _ in
  3491. proceedPinned(replace: true)
  3492. })
  3493. alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
  3494. })
  3495. self.present(alert, animated: true, completion: nil)
  3496. } else {
  3497. proceedPinned()
  3498. }
  3499. })
  3500. func proceedPinned(replace: Bool = false) {
  3501. if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
  3502. DispatchQueue.main.async {
  3503. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  3504. imageView.tintColor = .white
  3505. let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  3506. banner.show()
  3507. }
  3508. return
  3509. }
  3510. if replace {
  3511. checkDataPinned.sort {
  3512. let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
  3513. let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
  3514. return firstPinned < secondPinned
  3515. }
  3516. self.proceedPinUnpinMessage(checkDataPinned: checkDataPinned[0], isPinned: false) { res1 in
  3517. if res1 {
  3518. self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: true) { res2 in
  3519. if res2 {
  3520. let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  3521. DispatchQueue.main.async {
  3522. self.pinAllMessages(dataMessages: dataMessagesPin)
  3523. }
  3524. }
  3525. }
  3526. }
  3527. }
  3528. } else {
  3529. self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: true) { res in
  3530. if res {
  3531. let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  3532. DispatchQueue.main.async {
  3533. self.pinAllMessages(dataMessages: dataMessagesPin)
  3534. }
  3535. }
  3536. }
  3537. }
  3538. }
  3539. })
  3540. } else {
  3541. pin = UIAction(title: "Unpin".localized(), image: UIImage(systemName: "pin.slash"), handler: {(_) in
  3542. if self.removed {
  3543. return
  3544. }
  3545. if self.isSearching {
  3546. self.cancelAction()
  3547. }
  3548. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
  3549. self.proceedPinUnpinMessage(checkDataPinned: dataMessages[indexPath!.row], isPinned: false) { res in
  3550. if res {
  3551. let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  3552. DispatchQueue.main.async {
  3553. self.pinAllMessages(dataMessages: dataMessagesPin)
  3554. }
  3555. }
  3556. }
  3557. })
  3558. })
  3559. }
  3560. let replyP = UIAction(title: "Reply Privately".localized(), image: UIImage(systemName: "arrowshape.turn.up.left"), handler: {(_) in
  3561. if self.removed {
  3562. return
  3563. }
  3564. if self.isSearching {
  3565. self.cancelAction()
  3566. }
  3567. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
  3568. let f_pin = dataMessages[indexPath!.row]["f_pin"] as? String ?? ""
  3569. let message_id = dataMessages[indexPath!.row][TypeDataMessage.message_id] as? String ?? ""
  3570. if let dataSaved: String = SecureUserDefaults.shared.value(forKey: "new_saved_\(f_pin)") {
  3571. let data = dataSaved
  3572. if let jsonData = data.data(using: .utf8),
  3573. let dataJson = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: String] {
  3574. let last_m = dataJson["text"] ?? ""
  3575. let data: [String: String] = ["text": last_m, "reffId": message_id]
  3576. if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []),
  3577. let jsonString = String(data: jsonData, encoding: .utf8) {
  3578. SecureUserDefaults.shared.set(jsonString, forKey: "new_saved_\(f_pin)")
  3579. }
  3580. }
  3581. } else {
  3582. let data: [String: String] = ["text": "", "reffId": message_id]
  3583. if let jsonData = try? JSONSerialization.data(withJSONObject: data, options: []),
  3584. let jsonString = String(data: jsonData, encoding: .utf8) {
  3585. SecureUserDefaults.shared.set(jsonString, forKey: "new_saved_\(f_pin)")
  3586. }
  3587. }
  3588. let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
  3589. editorPersonalVC.hidesBottomBarWhenPushed = true
  3590. editorPersonalVC.unique_l_pin = f_pin
  3591. if let nav = self.navigationController {
  3592. nav.show(editorPersonalVC, sender: nil)
  3593. nav.viewControllers.remove(at: nav.viewControllers.count - 2)
  3594. }
  3595. })
  3596. })
  3597. let forward = UIAction(title: "Forward".localized(), image: UIImage(systemName: "arrowshape.turn.up.right"), handler: {(_) in
  3598. if self.removed {
  3599. return
  3600. }
  3601. if self.isSearching {
  3602. self.cancelAction()
  3603. }
  3604. if self.reffId != nil {
  3605. self.deleteReplyView()
  3606. }
  3607. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
  3608. self.forwardSession = true
  3609. let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
  3610. if !self.isHistoryCC {
  3611. self.navigationItem.rightBarButtonItems = nil
  3612. }
  3613. self.navigationItem.rightBarButtonItem = cancelButton
  3614. self.changeAppBar()
  3615. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
  3616. if idx != nil{
  3617. self.dataMessages[idx!]["isSelected"] = true
  3618. }
  3619. self.addMultipleSelectSession()
  3620. self.tableChatView.reloadData()
  3621. }
  3622. })
  3623. let copy = UIAction(title: "Copy".localized(), image: UIImage(systemName: "doc.on.doc"), handler: {(_) in
  3624. if self.removed {
  3625. return
  3626. }
  3627. if self.isSearching {
  3628. self.cancelAction()
  3629. }
  3630. if self.reffId != nil {
  3631. self.deleteReplyView()
  3632. }
  3633. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
  3634. self.copySession = true
  3635. let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
  3636. if !self.isHistoryCC {
  3637. self.navigationItem.rightBarButtonItems = nil
  3638. }
  3639. self.navigationItem.rightBarButtonItem = cancelButton
  3640. self.changeAppBar()
  3641. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
  3642. if idx != nil{
  3643. self.dataMessages[idx!]["isSelected"] = true
  3644. }
  3645. self.addMultipleSelectSession()
  3646. self.tableChatView.reloadData()
  3647. }
  3648. })
  3649. let edit = UIAction(title: "Edit".localized(), image: UIImage(systemName: "pencil.tip.crop.circle"), handler: {(_) in
  3650. self.isEditingMessage = true
  3651. self.showEditMessageView(at: indexPath!)
  3652. })
  3653. let translate = UIAction(title: "Translate".localized(), image: UIImage(systemName: "t.bubble"), handler: {(_) in
  3654. self.view.makeToast("Translating...".localized(), duration: 3)
  3655. var translation: String = "English"
  3656. let lang: String = SecureUserDefaults.shared.value(forKey: "i18n_language") ?? "en"
  3657. if lang == "id" {
  3658. translation = "Indonesia"
  3659. }
  3660. let payload: [String : Any] = [
  3661. "role": "user",
  3662. "content": dataMessages[indexPath!.row][TypeDataMessage.message_text]!!
  3663. ]
  3664. let parameter: [String : Any] = [
  3665. "use_video": "0",
  3666. "translate": translation,
  3667. "payload": [payload]
  3668. ]
  3669. DispatchQueue.global().async {
  3670. Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getGPTBotUrl())!, parameter: parameter, completion: { data, response, error in
  3671. let response = response as? HTTPURLResponse
  3672. if response?.statusCode != 200 || error != nil {
  3673. DispatchQueue.main.async {
  3674. self.view.makeToast("There is an error occurred while translating your message. Please try again or check your network connection.".localized(), duration: 3)
  3675. }
  3676. return
  3677. }
  3678. if let data = data, let responseString = String(data: data, encoding: .utf8) {
  3679. if let json = try? JSONSerialization.jsonObject(with: responseString.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: String] {
  3680. let dataContent = json["content"]!
  3681. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
  3682. if idx != nil{
  3683. self.dataMessages[idx!][TypeDataMessage.message_text] = (dataMessages[indexPath!.row][TypeDataMessage.message_text] as? String ?? "") + "\n\n" + "$\(dataContent)$"
  3684. }
  3685. DispatchQueue.main.async{
  3686. self.tableChatView.reloadRows(at: [indexPath!], with: .none)
  3687. }
  3688. }
  3689. }
  3690. })
  3691. }
  3692. })
  3693. let gcs = UIAction(title: "Get Chat Suggestion".localized(), image: UIImage(systemName: "exclamationmark.bubble"), handler: {(_) in
  3694. self.view.makeToast("Getting chat suggestion...".localized(), duration: 3)
  3695. let payload: [String : Any] = [
  3696. "role": "user",
  3697. "content": dataMessages[indexPath!.row][TypeDataMessage.message_text]!!
  3698. ]
  3699. let parameter: [String : Any] = [
  3700. "use_video": "0",
  3701. "suggest": "1",
  3702. "payload": [payload]
  3703. ]
  3704. DispatchQueue.global().async {
  3705. Utils.postDataWithCookiesAndUserAgent(from: URL(string: Utils.getGPTBotUrl())!, parameter: parameter, completion: { data, response, error in
  3706. let response = response as? HTTPURLResponse
  3707. if response?.statusCode != 200 || error != nil {
  3708. DispatchQueue.main.async {
  3709. self.view.makeToast("There is an error occurred while getting chat suggestion for you. Please try again or check your network connection.".localized(), duration: 3)
  3710. }
  3711. return
  3712. }
  3713. if let data = data, let responseString = String(data: data, encoding: .utf8) {
  3714. if let json = try? JSONSerialization.jsonObject(with: responseString.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: Any] {
  3715. if let dataMessage = json["message"] as? [[String: Any]] {
  3716. if let dataContent = dataMessage[0]["content"] as? String {
  3717. DispatchQueue.main.async{
  3718. self.textFieldSend.text = dataContent
  3719. self.textFieldSend.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : UIColor.black
  3720. }
  3721. }
  3722. }
  3723. }
  3724. }
  3725. })
  3726. }
  3727. })
  3728. let more = UIMenu(title: "More...".localized(), children: [translate, gcs])
  3729. let info = UIAction(title: "Info".localized(), image: UIImage(systemName: "info.circle"), handler: {(_) in
  3730. if self.removed {
  3731. return
  3732. }
  3733. let messageInfoVC = MessageInfo()
  3734. messageInfoVC.data = dataMessages[indexPath!.row]
  3735. messageInfoVC.dataGroup = self.dataGroup
  3736. messageInfoVC.isPersonal = false
  3737. self.navigationController?.pushViewController(messageInfoVC, animated: true)
  3738. })
  3739. let delete = UIAction(title: "Delete".localized(), image: UIImage(systemName: "trash"), attributes: .destructive, handler: {(_) in
  3740. if self.removed {
  3741. return
  3742. }
  3743. if self.isSearching {
  3744. self.cancelAction()
  3745. }
  3746. if self.reffId != nil {
  3747. self.deleteReplyView()
  3748. }
  3749. DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
  3750. self.deleteSession = true
  3751. let cancelButton = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(self.cancelAction))
  3752. if !self.isHistoryCC {
  3753. self.navigationItem.rightBarButtonItems = nil
  3754. }
  3755. self.navigationItem.rightBarButtonItem = cancelButton
  3756. self.changeAppBar()
  3757. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[indexPath!.row]["message_id"] as? String})
  3758. if idx != nil{
  3759. self.dataMessages[idx!]["isSelected"] = true
  3760. }
  3761. self.addMultipleSelectSession()
  3762. self.tableChatView.reloadData()
  3763. }
  3764. })
  3765. let resend = UIAction(title: "Resend".localized(), image: UIImage(systemName: "arrow.clockwise"), handler: {(_) in
  3766. let messageId = dataMessages[indexPath!.row][TypeDataMessage.message_id] as? String ?? ""
  3767. let status = dataMessages[indexPath!.row][TypeDataMessage.status] as? String ?? ""
  3768. var idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == messageId })
  3769. if let idxMessageIdParent = self.groupImages.firstIndex(where: { $0.value.contains(where: { $0.messageId == messageId }) }) {
  3770. if let idxInImages = self.groupImages[idxMessageIdParent].value.firstIndex(where: { $0.messageId == messageId }) {
  3771. self.groupImages[idxMessageIdParent].value[idxInImages].status = "1"
  3772. self.groupImages[idxMessageIdParent].value[idxInImages].dataMessage[TypeDataMessage.status] = "1"
  3773. }
  3774. idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == self.groupImages[idxMessageIdParent].key })
  3775. }
  3776. if (idx != nil) {
  3777. do {
  3778. self.dataMessages[idx!][TypeDataMessage.status] = "1"
  3779. self.dataMessages[idx!][TypeDataMessage.progress] = 0.0
  3780. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  3781. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String == self.dataMessages[idx!]["message_id"] as? String })
  3782. if row != nil && section != nil {
  3783. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  3784. }
  3785. } catch {
  3786. }
  3787. }
  3788. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3789. do {
  3790. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  3791. "status" : "1"
  3792. ], _where: "message_id = '\(messageId)'")
  3793. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE_STATUS", cvalues: [
  3794. "status" : "1"
  3795. ], _where: "message_id = '\(messageId)'")
  3796. } catch {
  3797. rollback.pointee = true
  3798. print("Access database error: \(error.localizedDescription)")
  3799. }
  3800. })
  3801. let message = CoreMessage_TMessageBank.sendMessage(message_id: messageId,
  3802. l_pin: dataMessages[indexPath!.row][TypeDataMessage.l_pin] as? String ?? "",
  3803. message_scope_id: dataMessages[indexPath!.row][TypeDataMessage.message_scope_id] as? String ?? "",
  3804. status: "1",
  3805. message_text: dataMessages[indexPath!.row][TypeDataMessage.message_text] as? String ?? "",
  3806. credential: dataMessages[indexPath!.row][TypeDataMessage.credential] as? String ?? "",
  3807. attachment_flag: dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "",
  3808. ex_blog_id: dataMessages[indexPath!.row][TypeDataMessage.blog_id] as? String ?? "",
  3809. message_large_text: "",
  3810. ex_format: "",
  3811. image_id: dataMessages[indexPath!.row][TypeDataMessage.image_id] as? String ?? "",
  3812. audio_id: dataMessages[indexPath!.row][TypeDataMessage.audio_id] as? String ?? "",
  3813. video_id: dataMessages[indexPath!.row][TypeDataMessage.video_id] as? String ?? "",
  3814. file_id: dataMessages[indexPath!.row][TypeDataMessage.file_id] as? String ?? "",
  3815. thumb_id: dataMessages[indexPath!.row][TypeDataMessage.thumb_id] as? String ?? "",
  3816. reff_id: dataMessages[indexPath!.row][TypeDataMessage.reff_id] as? String ?? "",
  3817. read_receipts: dataMessages[indexPath!.row][TypeDataMessage.read_receipts] as? String ?? "",
  3818. chat_id: dataMessages[indexPath!.row][TypeDataMessage.chat_id] as? String ?? "",
  3819. is_call_center: dataMessages[indexPath!.row][TypeDataMessage.is_call_center] as? String ?? "",
  3820. call_center_id: dataMessages[indexPath!.row][TypeDataMessage.call_center_id] as? String ?? "",
  3821. opposite_pin: dataMessages[indexPath!.row][TypeDataMessage.opposite_pin] as? String ?? "", specFile: "")
  3822. Nexilis.addQueueMessage(message: message)
  3823. })
  3824. var children: [UIMenuElement] = [star, reply, pin, copy, delete]
  3825. var isMore = false
  3826. // let copyOption = self.copyOption(indexPath: indexPath!)
  3827. let idMe = User.getMyPin() as String?
  3828. if dataMessages[indexPath!.row]["status"] as? String ?? "" == "0" {
  3829. children = [resend, delete]
  3830. } else if (dataMessages[indexPath!.row]["lock"] != nil && dataMessages[indexPath!.row]["lock"] as? String ?? "" == "1") || dataMessages[indexPath!.row]["message_scope_id"] as? String ?? "" == "18" || dataMessages[indexPath!.row]["credential"] as? String ?? "" == "1" {
  3831. children = [delete]
  3832. } else if (groupImages[dataMessages[indexPath!.row]["message_id"] as? String ?? ""] != nil) {
  3833. forward.title = "Forward All".localized()
  3834. delete.title = "Delete All".localized()
  3835. children = [delete]
  3836. if (Nexilis.checkingAccess(key: "secure_folder_forward") || (dataMessages[indexPath!.row][TypeDataMessage.spec_file] as? String ?? "").contains("forward")) && dataMessages[indexPath!.row]["read_receipts"] as? String != "8" {
  3837. children.insert(forward, at: 0)
  3838. }
  3839. } else {
  3840. if dataMessages[indexPath!.row]["f_pin"] as? String ?? "" == "-999" {
  3841. children = [star, reply ,delete]
  3842. }
  3843. else if !(dataMessages[indexPath!.row]["image_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["video_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath!.row]["file_id"] as? String ?? "").isEmpty {
  3844. children = [star, reply, pin, delete]
  3845. } else if dataMessages[indexPath!.row]["attachment_flag"] as? String ?? "" == "11" {
  3846. children = [reply, pin, delete]
  3847. }
  3848. if (Nexilis.checkingAccess(key: "secure_folder_forward") || (!(dataMessages[indexPath!.row][TypeDataMessage.message_text] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["image_id"] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["video_id"] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["file_id"] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["audio_id"] as? String ?? "").isEmpty) || (dataMessages[indexPath!.row][TypeDataMessage.spec_file] as? String ?? "").contains("forward")) && dataMessages[indexPath!.row]["read_receipts"] as? String != "8" && dataMessages[indexPath!.row]["attachment_flag"] as? String ?? "" != "11" {
  3849. children.insert(forward, at: 2)
  3850. }
  3851. if dataMessages[indexPath!.row]["f_pin"] as? String ?? "" != "-999" && dataMessages[indexPath!.row]["f_pin"] as? String != User.getMyPin() && dataMessages[indexPath!.row]["attachment_flag"] as? String ?? "" != "11" {
  3852. children.insert(replyP, at: 2)
  3853. }
  3854. if (dataMessages[indexPath!.row]["f_pin"] as? String ?? "") == idMe {
  3855. children.insert(info, at: children.count - 1)
  3856. }
  3857. if !(dataMessages[indexPath!.row][TypeDataMessage.message_text] as? String ?? "").isEmpty {
  3858. if (dataMessages[indexPath!.row]["f_pin"] as? String ?? "") == idMe && ((dataMessages[indexPath!.row][TypeDataMessage.is_forwarded] as? Int) ?? 0) == 0 && (dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "") != "11" {
  3859. let date = Date(milliseconds: Int64(dataMessages[indexPath!.row][TypeDataMessage.server_date] as? String ?? "") ?? 0)
  3860. let pastDate = date.addingTimeInterval(-10 * 60)
  3861. let differenceInSeconds = date.timeIntervalSince(pastDate)
  3862. if abs(differenceInSeconds) <= 15 * 60 {
  3863. children.insert(edit, at: children.count - 1)
  3864. }
  3865. }
  3866. if (dataMessages[indexPath!.row][TypeDataMessage.attachment_flag] as? String ?? "") != "11" && (dataMessages[indexPath!.row]["image_id"] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["video_id"] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["file_id"] as? String ?? "").isEmpty && (dataMessages[indexPath!.row]["audio_id"] as? String ?? "").isEmpty{
  3867. isMore = true
  3868. }
  3869. }
  3870. }
  3871. let mainMenu = UIMenu(title: "", options: [.displayInline],
  3872. children: children)
  3873. var menuForShow = UIMenu(title: "", children: [mainMenu])
  3874. if isMore {
  3875. menuForShow = UIMenu(title: "", children: [mainMenu, more])
  3876. }
  3877. return UIContextMenuConfiguration(identifier: nil,
  3878. previewProvider: nil) { _ in
  3879. return menuForShow
  3880. }
  3881. }
  3882. func proceedPinUnpinMessage(checkDataPinned: [String: Any?], isPinned: Bool, completion: @escaping (Bool)-> Void) {
  3883. DispatchQueue.global().async {
  3884. var jaData = [[String: Any]]()
  3885. var jsonObject = [String: Any]()
  3886. jsonObject[CoreMessage_TMessageKey.MESSAGE_ID] = checkDataPinned["message_id"] as? String ?? ""
  3887. jsonObject[CoreMessage_TMessageKey.IS_PINNED_MESSAGE] = isPinned ? "\(Date().currentTimeMillis())" : "0"
  3888. jaData.append(jsonObject)
  3889. if let jsonData = try? JSONSerialization.data(withJSONObject: jaData, options: []),
  3890. let jsonString = String(data: jsonData, encoding: .utf8) {
  3891. if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getPinMessage(f_pin: User.getMyPin() ?? "", data: jsonString, oppositePin: self.dataGroup["group_id"] as? String ?? "", chatId: self.dataTopic["chat_id"] as? String ?? "", scopeId: MessageScope.GROUP)) {
  3892. if response.isOk() {
  3893. if isPinned {
  3894. let mId = Nexilis.saveMessageNotif(textMessage: "You".localized() + " " + "pinned a message".localized(), fPin: User.getMyPin() ?? "", lPin: self.unique_l_pin, chatId: self.dataTopic["chat_id"] as? String ?? "", scopeId: MessageScope.GROUP)
  3895. self.appendNewMessage(messageId: mId)
  3896. }
  3897. DispatchQueue.global().async {
  3898. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3899. do {
  3900. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  3901. "is_pinned" : isPinned ? "\(Date().currentTimeMillis())" : "0"
  3902. ], _where: "message_id = '\(checkDataPinned["message_id"] as? String ?? "")'")
  3903. } catch {
  3904. rollback.pointee = true
  3905. print("Access database error: \(error.localizedDescription)")
  3906. }
  3907. })
  3908. }
  3909. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == checkDataPinned["message_id"] as? String ?? ""})
  3910. if idx != nil{
  3911. self.dataMessages[idx!][TypeDataMessage.is_pinned] = isPinned ? "\(Date().currentTimeMillis())" : "0"
  3912. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  3913. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataMessages[idx!]["chat_date"] as? String ?? ""}).firstIndex(where: { $0["message_id"] as? String ?? "" == self.dataMessages[idx!]["message_id"] as? String ?? "" })
  3914. if row != nil && section != nil {
  3915. DispatchQueue.main.async {
  3916. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  3917. }
  3918. }
  3919. }
  3920. completion(true)
  3921. } else {
  3922. DispatchQueue.main.async {
  3923. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  3924. imageView.tintColor = .white
  3925. let banner = FloatingNotificationBanner(title: "Failed to pin or unpin message, make sure you are connected to internet".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  3926. banner.show()
  3927. }
  3928. completion(false)
  3929. }
  3930. } else {
  3931. DispatchQueue.main.async {
  3932. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  3933. imageView.tintColor = .white
  3934. let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  3935. banner.show()
  3936. }
  3937. completion(false)
  3938. }
  3939. }
  3940. }
  3941. }
  3942. private func appendNewMessage(messageId: String) {
  3943. var row: [String: Any?] = [:]
  3944. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  3945. if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT message_id, f_pin, l_pin, message_scope_id, server_date, status, message_text, audio_id, video_id, image_id, thumb_id, read_receipts, chat_id, file_id, attachment_flag, reff_id, lock, is_stared, blog_id, credential, is_call_center, call_center_id, opposite_pin, last_edited, gif_id, is_forwarded_message, attachment_speciality, is_pinned from MESSAGE where message_id = '\(messageId)'"), cursorData.next() {
  3946. row["message_id"] = cursorData.string(forColumnIndex: 0)
  3947. row["f_pin"] = cursorData.string(forColumnIndex: 1)
  3948. row["l_pin"] = cursorData.string(forColumnIndex: 2)
  3949. row["message_scope_id"] = cursorData.string(forColumnIndex: 3)
  3950. row["server_date"] = cursorData.string(forColumnIndex: 4)
  3951. row["status"] = cursorData.string(forColumnIndex: 5)
  3952. row["message_text"] = cursorData.string(forColumnIndex: 6)
  3953. row["audio_id"] = cursorData.string(forColumnIndex: 7)
  3954. row["video_id"] = cursorData.string(forColumnIndex: 8)
  3955. row["image_id"] = cursorData.string(forColumnIndex: 9)
  3956. row["thumb_id"] = cursorData.string(forColumnIndex: 10)
  3957. row["read_receipts"] = cursorData.string(forColumnIndex: 11)
  3958. row["chat_id"] = cursorData.string(forColumnIndex: 12)
  3959. row["file_id"] = cursorData.string(forColumnIndex: 13)
  3960. row["attachment_flag"] = cursorData.string(forColumnIndex: 14)
  3961. row["reff_id"] = cursorData.string(forColumnIndex: 15)
  3962. row["lock"] = cursorData.string(forColumnIndex: 16)
  3963. row["is_stared"] = cursorData.string(forColumnIndex: 17)
  3964. row["blog_id"] = cursorData.string(forColumnIndex: 18)
  3965. row["credential"] = cursorData.string(forColumnIndex: 19)
  3966. row[TypeDataMessage.is_call_center] = cursorData.string(forColumnIndex: 20)
  3967. row[TypeDataMessage.call_center_id] = cursorData.string(forColumnIndex: 21)
  3968. row[TypeDataMessage.opposite_pin] = cursorData.string(forColumnIndex: 22)
  3969. row[TypeDataMessage.last_edit] = cursorData.longLongInt(forColumnIndex: 23)
  3970. row[TypeDataMessage.gif_id] = cursorData.string(forColumnIndex: 24)
  3971. row[TypeDataMessage.is_forwarded] = Int(cursorData.int(forColumnIndex: 25))
  3972. row[TypeDataMessage.spec_file] = cursorData.string(forColumnIndex: 26)
  3973. row[TypeDataMessage.is_pinned] = cursorData.string(forColumnIndex: 27)
  3974. row["progress"] = 0.0
  3975. row["isSelected"] = false
  3976. row["chat_date"] = "Today".localized()
  3977. cursorData.close()
  3978. }
  3979. })
  3980. DispatchQueue.main.async {
  3981. if !self.dataDates.contains("Today".localized()) {
  3982. self.dataDates.append("Today".localized())
  3983. self.tableChatView.insertSections(IndexSet(integer: self.dataDates.count - 1), with: .none)
  3984. }
  3985. self.tableChatView.beginUpdates()
  3986. self.dataMessages.append(row)
  3987. self.tableChatView.insertRows(at: [IndexPath(row: self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[self.dataDates.count - 1]}).count - 1, section: self.dataDates.count - 1)], with: .none)
  3988. self.tableChatView.endUpdates()
  3989. }
  3990. }
  3991. func showEditMessageView(at indexPath: IndexPath) {
  3992. tempListMentionWithText = listMentionWithText
  3993. tempListMentionInTextField = listMentionInTextField
  3994. listMentionWithText.removeAll()
  3995. listMentionInTextField.removeAll()
  3996. let dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexPath.section]})
  3997. let oldText = dataMessages[indexPath.row][TypeDataMessage.message_text] as? String ?? ""
  3998. var oldTextForTextview = oldText
  3999. let pattern = "@[\\w]+"
  4000. do {
  4001. let regex = try NSRegularExpression(pattern: pattern)
  4002. let nsrange = NSRange(oldText.startIndex..., in: oldText)
  4003. let matches = regex.matches(in: oldText, range: nsrange)
  4004. let results = matches.map {
  4005. String(oldText[Range($0.range, in: oldText)!])
  4006. }
  4007. for result in results {
  4008. let pinRes = result.components(separatedBy: "@")[1]
  4009. Database.shared.database?.inTransaction({ fmdb, rollback in
  4010. do {
  4011. if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name || ' ' || ifnull(last_name, '') name FROM GROUPZ_MEMBER where f_pin = '\(pinRes)'"), cursor.next() {
  4012. let user = User(pin: "")
  4013. user.pin = cursor.string(forColumnIndex: 0) ?? ""
  4014. user.firstName = cursor.string(forColumnIndex: 1) ?? ""
  4015. if !user.pin.isEmpty {
  4016. var fixUser = User.getDataCanNil(pin: user.pin, fmdb: fmdb)
  4017. if fixUser == nil {
  4018. fixUser = user
  4019. }
  4020. var indexAt = 0
  4021. if let range = oldTextForTextview.range(of: result) {
  4022. indexAt = oldTextForTextview.distance(from: oldTextForTextview.startIndex, to: range.lowerBound)
  4023. }
  4024. fixUser?.ex_block = "\(indexAt + fixUser!.fullName.count)"
  4025. listMentionWithText.append(fixUser!)
  4026. listMentionInTextField.append(fixUser!)
  4027. oldTextForTextview = oldTextForTextview.replacingOccurrences(of: result, with: "@\(fixUser!.fullName)")
  4028. lastTextLength = oldTextForTextview.count
  4029. }
  4030. cursor.close()
  4031. }
  4032. } catch {
  4033. rollback.pointee = true
  4034. print("Access database error: \(error.localizedDescription)")
  4035. }
  4036. })
  4037. }
  4038. } catch {
  4039. print("Invalid regex pattern")
  4040. }
  4041. editVC = UIViewController()
  4042. if let view = editVC.view {
  4043. // let tapGesture = ObjectGesture(target: self, action: #selector(dismissEditVC))
  4044. // tapGesture.message_id = oldTextForTextview
  4045. // view.addGestureRecognizer(tapGesture)
  4046. view.backgroundColor = .clear
  4047. let blurView = UIView()
  4048. let blurEffect = UIBlurEffect(style: .systemUltraThinMaterialLight)
  4049. let blurEffectView = UIVisualEffectView(effect: blurEffect)
  4050. blurEffectView.frame = blurView.bounds
  4051. blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  4052. blurView.addSubview(blurEffectView)
  4053. blurView.sendSubviewToBack(blurEffectView)
  4054. view.addSubview(blurView)
  4055. blurView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor)
  4056. let tapGesture = ObjectGesture(target: self, action: #selector(dismissEditVC))
  4057. tapGesture.message_id = oldTextForTextview
  4058. blurView.addGestureRecognizer(tapGesture)
  4059. editTextView = CustomTextView()
  4060. editTextView.layer.cornerRadius = textFieldSend.maxCornerRadius()
  4061. editTextView.layer.borderWidth = 1.0
  4062. editTextView.textColor = UIColor.black
  4063. editTextView.tintColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  4064. editTextView.textContainerInset = UIEdgeInsets(top: 12, left: 20, bottom: 11, right: 40)
  4065. editTextView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
  4066. editTextView.font = UIFont.systemFont(ofSize: 12 + offset())
  4067. editTextView.delegate = self
  4068. editTextView.allowsEditingTextAttributes = true
  4069. editTextView.backgroundColor = .clear
  4070. view.addSubview(editTextView)
  4071. editTextView.anchor(left: view.leftAnchor, right: view.rightAnchor, paddingLeft: 15, paddingRight: 15)
  4072. constraintBottomeditTextView = editTextView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -15)
  4073. constraintHeighteditTextView = editTextView.heightAnchor.constraint(equalToConstant: 40)
  4074. constraintBottomeditTextView.isActive = true
  4075. constraintHeighteditTextView.isActive = true
  4076. editTextView.attributedText = oldTextForTextview.richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "", listMentionInTextField: listMentionInTextField)
  4077. editTextView.becomeFirstResponder()
  4078. buttonSendEdit.setImage(resizeImage(image: self.traitCollection.userInterfaceStyle == .dark ? UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(.blackDarkMode) : UIImage(named: "Send-(White)", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, targetSize: CGSize(width: 30, height: 30)).withRenderingMode(.alwaysOriginal), for: .normal)
  4079. buttonSendEdit.circle()
  4080. buttonSendEdit.isEnabled = true
  4081. buttonSendEdit.actionHandle(controlEvents: .touchUpInside,
  4082. ForAction:{() -> Void in
  4083. var newText = self.editTextView.text ?? ""
  4084. if newText.contains("@") && self.listMentionInTextField.count > 0 {
  4085. var diff: Int = 0
  4086. for i in 0..<self.listMentionInTextField.count {
  4087. let mention = self.listMentionInTextField[i]
  4088. guard let exBlockStr = mention.ex_block, let exBlock = Int(exBlockStr) else {
  4089. continue // skip if ex_block is nil or not an integer
  4090. }
  4091. let nameWithMention = ("@" + mention.firstName + " " + mention.lastName).trimmingCharacters(in: .whitespaces)
  4092. let pinString = "@\(mention.pin)"
  4093. let upperBound = exBlock + diff
  4094. let lowerBound = upperBound - nameWithMention.count + 1
  4095. guard lowerBound >= 0, upperBound < newText.count else {
  4096. continue // prevent index out-of-range
  4097. }
  4098. var afterMention = ""
  4099. let nextCharIndex = newText.index(newText.startIndex, offsetBy: upperBound + 1, limitedBy: newText.endIndex)
  4100. if let index = nextCharIndex, index < newText.endIndex {
  4101. let nextChar = newText[index]
  4102. if nextChar != "\n" && nextChar != " " {
  4103. afterMention = " "
  4104. }
  4105. }
  4106. let startIndex = newText.index(newText.startIndex, offsetBy: lowerBound)
  4107. let endIndex = newText.index(newText.startIndex, offsetBy: upperBound + 1)
  4108. let range = startIndex..<endIndex
  4109. if newText[range] == nameWithMention {
  4110. newText.replaceSubrange(range, with: pinString + afterMention)
  4111. diff += (pinString + afterMention).count - nameWithMention.count
  4112. }
  4113. }
  4114. }
  4115. if !newText.isEmpty && newText.trimmingCharacters(in: .whitespacesAndNewlines) != oldText {
  4116. let lastEdited = Int64(Date().currentTimeMillis())
  4117. let message = CoreMessage_TMessageBank.editMessage(message_id: dataMessages[indexPath.row][TypeDataMessage.message_id] as? String ?? "", l_pin: dataMessages[indexPath.row][TypeDataMessage.l_pin] as? String ?? "", message_scope_id: dataMessages[indexPath.row][TypeDataMessage.message_scope_id] as? String ?? "", status: "1", message_text: newText, credential: dataMessages[indexPath.row][TypeDataMessage.credential] as? String ?? "", attachment_flag: dataMessages[indexPath.row][TypeDataMessage.attachment_flag] as? String ?? "", ex_blog_id: dataMessages[indexPath.row][TypeDataMessage.blog_id] as? String ?? "", message_large_text: "", ex_format: "", image_id: dataMessages[indexPath.row][TypeDataMessage.image_id] as? String ?? "", audio_id: dataMessages[indexPath.row][TypeDataMessage.audio_id] as? String ?? "", video_id: dataMessages[indexPath.row][TypeDataMessage.video_id] as? String ?? "", file_id: dataMessages[indexPath.row][TypeDataMessage.file_id] as? String ?? "", thumb_id: dataMessages[indexPath.row][TypeDataMessage.thumb_id] as? String ?? "", reff_id: dataMessages[indexPath.row][TypeDataMessage.reff_id] as? String ?? "", read_receipts: dataMessages[indexPath.row][TypeDataMessage.read_receipts] as? String ?? "", chat_id: dataMessages[indexPath.row][TypeDataMessage.chat_id] as? String ?? "", is_call_center: dataMessages[indexPath.row][TypeDataMessage.is_call_center] as? String ?? "", call_center_id: dataMessages[indexPath.row][TypeDataMessage.call_center_id] as? String ?? "", opposite_pin: dataMessages[indexPath.row][TypeDataMessage.opposite_pin] as? String ?? "", server_date: dataMessages[indexPath.row][TypeDataMessage.server_date] as? String ?? "", local_time_stamp: dataMessages[indexPath.row][TypeDataMessage.server_date] as? String ?? "", last_edit: lastEdited)
  4118. Nexilis.addQueueMessage(message: message, isEditMessage: true)
  4119. DispatchQueue.global().async {
  4120. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  4121. do {
  4122. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  4123. "message_text" : newText,
  4124. "last_edited" : lastEdited
  4125. ], _where: "message_id = '\(dataMessages[indexPath.row]["message_id"] as? String ?? "")'")
  4126. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  4127. } catch {
  4128. rollback.pointee = true
  4129. print("Access database error: \(error.localizedDescription)")
  4130. }
  4131. })
  4132. }
  4133. let idx = self.dataMessages.firstIndex(where: { $0[TypeDataMessage.message_id] as? String == dataMessages[indexPath.row][TypeDataMessage.message_id] as? String})
  4134. if idx != nil{
  4135. self.dataMessages[idx!][TypeDataMessage.message_text] = newText
  4136. self.dataMessages[idx!][TypeDataMessage.last_edit] = lastEdited
  4137. self.tableChatView.reloadRows(at: [indexPath], with: .none)
  4138. }
  4139. }
  4140. self.isEditingMessage = false
  4141. self.listMentionWithText = self.tempListMentionWithText
  4142. self.listMentionInTextField = self.tempListMentionWithText
  4143. self.lastTextLength = self.textFieldSend.text?.count ?? 0
  4144. self.heightTableEditMention = nil
  4145. self.editVC.dismiss(animated: true)
  4146. })
  4147. buttonSendEdit.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .mainColor
  4148. view.addSubview(buttonSendEdit)
  4149. buttonSendEdit.anchor(right: view.rightAnchor, paddingRight: 15, width: 40, height: 40)
  4150. constraintBottomSendEditTV = buttonSendEdit.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -15)
  4151. constraintBottomSendEditTV.isActive = true
  4152. let viewMessage = UIView()
  4153. view.addSubview(viewMessage)
  4154. viewMessage.translatesAutoresizingMaskIntoConstraints = false
  4155. if (dataMessages[indexPath.row][TypeDataMessage.f_pin] as? String == User.getMyPin()) {
  4156. viewMessage.leftAnchor.constraint(greaterThanOrEqualTo: view.leftAnchor, constant: 60).isActive = true
  4157. viewMessage.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -15).isActive = true
  4158. viewMessage.backgroundColor = .blueBubbleColor
  4159. viewMessage.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMinXMinYCorner]
  4160. } else {
  4161. viewMessage.rightAnchor.constraint(lessThanOrEqualTo: view.rightAnchor, constant: 60).isActive = true
  4162. viewMessage.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 15).isActive = true
  4163. viewMessage.backgroundColor = .whiteBubbleColor
  4164. viewMessage.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner]
  4165. }
  4166. viewMessage.bottomAnchor.constraint(equalTo: editTextView.topAnchor, constant: -15).isActive = true
  4167. viewMessage.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
  4168. viewMessage.widthAnchor.constraint(greaterThanOrEqualToConstant: 46).isActive = true
  4169. viewMessage.layer.cornerRadius = 10.0
  4170. viewMessage.clipsToBounds = true
  4171. let messageText = UILabel()
  4172. messageText.numberOfLines = 0
  4173. messageText.lineBreakMode = .byWordWrapping
  4174. viewMessage.addSubview(messageText)
  4175. messageText.translatesAutoresizingMaskIntoConstraints = false
  4176. messageText.topAnchor.constraint(equalTo: viewMessage.topAnchor, constant: 15).isActive = true
  4177. messageText.leadingAnchor.constraint(equalTo: viewMessage.leadingAnchor, constant: 15).isActive = true
  4178. messageText.bottomAnchor.constraint(equalTo: viewMessage.bottomAnchor, constant: -15).isActive = true
  4179. messageText.trailingAnchor.constraint(equalTo: viewMessage.trailingAnchor, constant: -15).isActive = true
  4180. messageText.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  4181. messageText.font = .systemFont(ofSize: 12 + offset())
  4182. messageText.attributedText = oldText.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  4183. tableMentionEdit = UITableView()
  4184. tableMentionEdit.register(UITableViewCell.self, forCellReuseIdentifier: "cellEditMention")
  4185. tableMentionEdit.dataSource = self
  4186. tableMentionEdit.delegate = self
  4187. tableMentionEdit.contentInset = UIEdgeInsets(top: -25, left: 0, bottom: 0, right: 0)
  4188. tableMentionEdit.backgroundColor = .white
  4189. view.addSubview(tableMentionEdit)
  4190. tableMentionEdit.anchor(left: view.leftAnchor, bottom: editTextView.topAnchor, right: view.rightAnchor)
  4191. heightTableEditMention = tableMentionEdit.heightAnchor.constraint(equalToConstant: 0)
  4192. self.heightTableEditMention.isActive = true
  4193. }
  4194. editVC.modalTransitionStyle = .crossDissolve
  4195. editVC.modalPresentationStyle = .overFullScreen
  4196. self.present(editVC, animated: true, completion: {
  4197. self.constraintHeighteditTextView.constant = self.editTextView.contentSize.height
  4198. if self.constraintHeighteditTextView.constant > 95 {
  4199. self.constraintHeighteditTextView.constant = 95.0
  4200. }
  4201. })
  4202. }
  4203. @objc func dismissEditVC(_ sender: ObjectGesture) {
  4204. if editTextView.text == sender.message_id {
  4205. isEditingMessage = false
  4206. listMentionWithText = tempListMentionWithText
  4207. listMentionInTextField = tempListMentionWithText
  4208. lastTextLength = textFieldSend.text?.count ?? 0
  4209. heightTableEditMention = nil
  4210. editVC.dismiss(animated: true)
  4211. } else if self.isEditingMessage {
  4212. let alert = LibAlertController(title: "".localized(), message: "Discard edit?".localized(), preferredStyle: .alert)
  4213. alert.addAction(UIAlertAction(title: "Cancel".localized(), style: UIAlertAction.Style.cancel, handler: nil))
  4214. alert.addAction(UIAlertAction(title: "Discard".localized(), style: UIAlertAction.Style.default, handler: {(_) in
  4215. self.isEditingMessage = false
  4216. self.listMentionWithText = self.tempListMentionWithText
  4217. self.listMentionInTextField = self.tempListMentionWithText
  4218. self.lastTextLength = self.textFieldSend.text?.count ?? 0
  4219. self.heightTableEditMention = nil
  4220. self.editVC.dismiss(animated: true)
  4221. }))
  4222. editVC.present(alert, animated: true, completion: nil)
  4223. } else {
  4224. lastTextLength = self.textFieldSend.text?.count ?? 0
  4225. editVC.dismiss(animated: true)
  4226. }
  4227. }
  4228. @objc func cancelAction() {
  4229. DispatchQueue.main.async {
  4230. if self.copySession {
  4231. self.copySession = false
  4232. } else if self.forwardSession {
  4233. self.forwardSession = false
  4234. } else if self.deleteSession {
  4235. self.deleteSession = false
  4236. } else if self.isSearching {
  4237. self.countMatchesSearch = 0
  4238. self.isSearching = false
  4239. }
  4240. if self.viewTextfield.isHidden {
  4241. self.viewTextfield.isHidden = false
  4242. }
  4243. if self.viewAttachment.isHidden {
  4244. self.viewAttachment.isHidden = false
  4245. }
  4246. if self.containerAction.isHidden {
  4247. self.containerAction.isHidden = false
  4248. }
  4249. if self.viewButton.isHidden {
  4250. self.viewButton.isHidden = false
  4251. }
  4252. if self.constraintBottomTableViewWithTextfield.constant == -60.0 {
  4253. self.constraintBottomTableViewWithTextfield.constant = self.constraintBottomTableViewWithTextfield.constant + 70
  4254. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
  4255. if (self.currentIndexpath != nil) {
  4256. self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: true)
  4257. } else {
  4258. self.tableChatView.scrollToBottom()
  4259. }
  4260. })
  4261. }
  4262. let data = self.dataMessages.filter({ $0["isSelected"] as! Bool == true })
  4263. for i in 0..<data.count {
  4264. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == data[i]["message_id"] as? String ?? ""})
  4265. if idx != nil{
  4266. self.dataMessages[idx!]["isSelected"] = false
  4267. }
  4268. }
  4269. self.tableChatView.reloadData()
  4270. self.setRightButtonItem()
  4271. self.changeAppBar()
  4272. self.containerMultpileSelectSession.removeFromSuperview()
  4273. self.checkNewMessage(tableView: self.tableChatView)
  4274. }
  4275. }
  4276. private func addMultipleSelectSession() {
  4277. viewTextfield.isHidden = true
  4278. viewAttachment.isHidden = true
  4279. containerAction.isHidden = true
  4280. viewButton.isHidden = true
  4281. constraintBottomTableViewWithTextfield.constant = constraintBottomTableViewWithTextfield.constant - 70
  4282. view.addSubview(containerMultpileSelectSession)
  4283. containerMultpileSelectSession.translatesAutoresizingMaskIntoConstraints = false
  4284. constraintBottomContainerMultpileSelectSession = containerMultpileSelectSession.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0)
  4285. NSLayoutConstraint.activate([
  4286. containerMultpileSelectSession.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
  4287. containerMultpileSelectSession.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
  4288. constraintBottomContainerMultpileSelectSession,
  4289. containerMultpileSelectSession.heightAnchor.constraint(equalToConstant: 50)
  4290. ])
  4291. containerMultpileSelectSession.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .white
  4292. addSubviewMultipleSession()
  4293. }
  4294. private func addSubviewMultipleSession() {
  4295. let container = UIView()
  4296. containerMultpileSelectSession.addSubview(container)
  4297. container.translatesAutoresizingMaskIntoConstraints = false
  4298. NSLayoutConstraint.activate([
  4299. container.leadingAnchor.constraint(equalTo: containerMultpileSelectSession.leadingAnchor),
  4300. container.trailingAnchor.constraint(equalTo:containerMultpileSelectSession.trailingAnchor),
  4301. container.bottomAnchor.constraint(equalTo: containerMultpileSelectSession.bottomAnchor),
  4302. container.heightAnchor.constraint(equalToConstant: 50)
  4303. ])
  4304. container.layer.shadowOpacity = 0.7
  4305. container.layer.shadowOffset = CGSize(width: 3, height: 3)
  4306. container.layer.shadowRadius = 3.0
  4307. container.layer.shadowColor = UIColor.black.cgColor
  4308. container.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .secondaryColor
  4309. if !isSearching {
  4310. let title = UILabel()
  4311. container.addSubview(title)
  4312. title.translatesAutoresizingMaskIntoConstraints = false
  4313. NSLayoutConstraint.activate([
  4314. title.centerXAnchor.constraint(equalTo: container.centerXAnchor),
  4315. title.centerYAnchor.constraint(equalTo:container.centerYAnchor),
  4316. ])
  4317. let countSelected = dataMessages.filter({ $0["isSelected"] as! Bool == true }).count
  4318. title.text = "\(countSelected) " + "Selected".localized()
  4319. title.textColor = .mainColor
  4320. title.font = UIFont.systemFont(ofSize: 15).bold
  4321. let button = UIImageView()
  4322. container.addSubview(button)
  4323. button.translatesAutoresizingMaskIntoConstraints = false
  4324. NSLayoutConstraint.activate([
  4325. button.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 15),
  4326. button.centerYAnchor.constraint(equalTo:container.centerYAnchor),
  4327. button.widthAnchor.constraint(equalToConstant: 30),
  4328. button.heightAnchor.constraint(equalToConstant: 30),
  4329. ])
  4330. if copySession {
  4331. button.image = UIImage(systemName: "doc.on.doc")
  4332. if countSelected == 0{
  4333. button.tintColor = .gray
  4334. } else {
  4335. button.tintColor = .mainColor
  4336. }
  4337. } else if forwardSession {
  4338. button.image = UIImage(systemName: "arrowshape.turn.up.right")
  4339. if countSelected == 0{
  4340. button.tintColor = .gray
  4341. } else {
  4342. button.tintColor = .mainColor
  4343. }
  4344. } else if deleteSession {
  4345. button.image = UIImage(systemName: "trash")
  4346. if countSelected == 0{
  4347. button.tintColor = .gray
  4348. } else {
  4349. button.tintColor = .red
  4350. }
  4351. }
  4352. let buttonGesture = UITapGestureRecognizer(target: self, action: #selector(sessionAction))
  4353. button.isUserInteractionEnabled = true
  4354. button.addGestureRecognizer(buttonGesture)
  4355. let selectedMessage = dataMessages.filter({ $0["isSelected"] as! Bool == true })
  4356. if selectedMessage.count > 0 {
  4357. for i in 0..<selectedMessage.count {
  4358. if let isGroupingImages = groupImages[selectedMessage[i]["message_id"] as? String ?? ""] {
  4359. title.text = "\(countSelected + (isGroupingImages.count - 1)) " + "Selected".localized()
  4360. }
  4361. }
  4362. }
  4363. } else {
  4364. buttonUp = UIButton()
  4365. container.addSubview(buttonUp)
  4366. buttonUp.translatesAutoresizingMaskIntoConstraints = false
  4367. NSLayoutConstraint.activate([
  4368. buttonUp.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 10),
  4369. buttonUp.centerYAnchor.constraint(equalTo:container.centerYAnchor),
  4370. buttonUp.widthAnchor.constraint(equalToConstant: 30),
  4371. buttonUp.heightAnchor.constraint(equalToConstant: 30),
  4372. ])
  4373. buttonUp.addTarget(self, action: #selector(upSearchText), for: .touchUpInside)
  4374. buttonDown = UIButton()
  4375. container.addSubview(buttonDown)
  4376. buttonDown.translatesAutoresizingMaskIntoConstraints = false
  4377. NSLayoutConstraint.activate([
  4378. buttonDown.leadingAnchor.constraint(equalTo: buttonUp.trailingAnchor, constant: 15),
  4379. buttonDown.centerYAnchor.constraint(equalTo:container.centerYAnchor),
  4380. buttonDown.widthAnchor.constraint(equalToConstant: 30),
  4381. buttonDown.heightAnchor.constraint(equalToConstant: 30),
  4382. ])
  4383. buttonDown.addTarget(self, action: #selector(downSearchText), for: .touchUpInside)
  4384. buttonUp.setImage(UIImage(systemName: "chevron.up"), for: .normal)
  4385. buttonUp.tintColor = .gray
  4386. buttonDown.setImage(UIImage(systemName: "chevron.down"), for: .normal)
  4387. buttonDown.tintColor = .gray
  4388. titleSearchMatches = UILabel()
  4389. container.addSubview(titleSearchMatches)
  4390. titleSearchMatches.translatesAutoresizingMaskIntoConstraints = false
  4391. NSLayoutConstraint.activate([
  4392. titleSearchMatches.centerXAnchor.constraint(equalTo: container.centerXAnchor),
  4393. titleSearchMatches.centerYAnchor.constraint(equalTo:container.centerYAnchor),
  4394. ])
  4395. titleSearchMatches.textColor = .mainColor
  4396. titleSearchMatches.font = UIFont.systemFont(ofSize: 15.0).bold
  4397. titleSearchMatches.isHidden = true
  4398. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
  4399. self.searchBar.becomeFirstResponder()
  4400. })
  4401. }
  4402. }
  4403. @objc func upSearchText() {
  4404. scrollToFirstSearchMessage(indexScroll: lastScrollIdxSearch + 1)
  4405. }
  4406. @objc func downSearchText() {
  4407. scrollToFirstSearchMessage(indexScroll: lastScrollIdxSearch - 1)
  4408. }
  4409. @objc func sessionAction() {
  4410. if copySession {
  4411. let dataMessages = self.dataMessages.filter({ $0["isSelected"] as! Bool == true })
  4412. let countSelected = dataMessages.count
  4413. if countSelected == 0 {
  4414. return
  4415. }
  4416. var nameTopic = "Lounge".localized()
  4417. if !dataTopic.isEmpty {
  4418. nameTopic = dataTopic["title"] as? String ?? ""
  4419. }
  4420. var text = "*^\(dataGroup["f_name"]!!) (\(nameTopic))^*"
  4421. for i in 0..<countSelected {
  4422. let stringDate = (dataMessages[i]["server_date"] as? String ?? "")
  4423. let date = Date(milliseconds: Int64(stringDate)!)
  4424. let formatterDate = DateFormatter()
  4425. let formatterTime = DateFormatter()
  4426. formatterDate.dateFormat = "dd/MM/yy"
  4427. formatterDate.locale = NSLocale(localeIdentifier: "id") as Locale?
  4428. formatterTime.dateFormat = "HH:mm"
  4429. formatterTime.locale = NSLocale(localeIdentifier: "id") as Locale?
  4430. let dataProfile = getDataProfile(f_pin: dataMessages[i]["f_pin"] as? String ?? "", message_id: dataMessages[i]["message_id"] as? String ?? "")
  4431. let textCopied = (dataMessages[i]["message_text"] as? String ?? "").richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "")
  4432. text = text + "\n\n*[\(formatterDate.string(from: date as Date)) \(formatterTime.string(from: date as Date))] \(dataProfile["name"]!):*\n\(textCopied.string)"
  4433. }
  4434. text = text + "\n\n\nchat " + "Powered by Nexilis".localized()
  4435. DispatchQueue.main.async {
  4436. UIPasteboard.general.string = text
  4437. self.view.makeToast("Text coppied to clipboard".localized(), duration: 3)
  4438. }
  4439. cancelAction()
  4440. } else if forwardSession {
  4441. var dataMessages = self.dataMessages.filter({ $0["isSelected"] as! Bool == true })
  4442. let countSelected = dataMessages.count
  4443. if countSelected == 0 {
  4444. return
  4445. }
  4446. for i in 0..<countSelected {
  4447. if let groupingImages = groupImages[dataMessages[i]["message_id"] as? String ?? ""] {
  4448. var tempData = dataMessages
  4449. tempData.remove(at: 0)
  4450. var dataMessageInGrouping = (groupImages[dataMessages[i]["message_id"] as? String ?? ""]!).map({ $0.dataMessage })
  4451. tempData.insert(contentsOf: dataMessageInGrouping, at: i)
  4452. dataMessages = tempData
  4453. }
  4454. }
  4455. contactChatNav.modalPresentationStyle = .custom
  4456. contactChatNav.navigationBar.tintColor = .white
  4457. contactChatNav.navigationBar.barTintColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
  4458. contactChatNav.navigationBar.isTranslucent = false
  4459. let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
  4460. contactChatNav.navigationBar.titleTextAttributes = textAttributes
  4461. let cancelButtonAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
  4462. UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes, for: .normal)
  4463. contactChatNav.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
  4464. if let controller = contactChatNav.viewControllers.first as? ContactChatViewController {
  4465. controller.isChooser = { [weak self] scope, pin in
  4466. if scope == MessageScope.WHISPER || scope == MessageScope.CALL || scope == MessageScope.MISSED_CALL {
  4467. let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
  4468. editorPersonalVC.unique_l_pin = pin
  4469. editorPersonalVC.dataMessageForward = dataMessages
  4470. self?.navigationController?.replaceAllViewController(with: editorPersonalVC, animated: true)
  4471. } else {
  4472. let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
  4473. editorGroupVC.unique_l_pin = pin
  4474. editorGroupVC.dataMessageForward = dataMessages
  4475. self?.navigationController?.replaceAllViewController(with: editorGroupVC, animated: true)
  4476. }
  4477. }
  4478. }
  4479. self.present(contactChatNav, animated: true, completion: nil)
  4480. } else if deleteSession {
  4481. let dataMessages = self.dataMessages.filter({ $0["isSelected"] as! Bool == true })
  4482. let countSelected = dataMessages.count
  4483. if countSelected == 0 {
  4484. return
  4485. }
  4486. let alertController = LibAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
  4487. if let action = self.actionDelete(for: "me", title: "Delete".localized() + " \(countSelected) " + "For Me".localized(), dataMessages: dataMessages) {
  4488. alertController.addAction(action)
  4489. }
  4490. let idMe = User.getMyPin() as String?
  4491. let dataFilterFpin = dataMessages.filter({ $0["f_pin"] as? String != idMe})
  4492. let dataFilterLock = dataMessages.filter({ $0["lock"] as? String == "1"})
  4493. // let statusDataRead = dataMessages.filter({ Int($0["status"] as? String ?? "")! >= 4})
  4494. let statusFailed = dataMessages.filter({ Int($0["status"] as? String ?? "")! == 0})
  4495. if dataFilterFpin.count == 0 && dataFilterLock.count == 0 && statusFailed.count == 0 {
  4496. if let action = self.actionDelete(for: "everyone", title: "Delete".localized() + " \(countSelected) " + "For Everyone".localized(), dataMessages: dataMessages) {
  4497. alertController.addAction(action)
  4498. }
  4499. }
  4500. alertController.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
  4501. self.present(alertController, animated: true)
  4502. }
  4503. }
  4504. private func deleteMessage(l_pin: String, message_id: String, scope: String, type: String, chat: String) {
  4505. let tmessage = CoreMessage_TMessageBank.deleteMessage(l_pin: l_pin, messageId: message_id, scope: scope, type: type, chat: chat)
  4506. Nexilis.deleteQueueMessage(message: tmessage)
  4507. }
  4508. private func queryMessageReply(message_id: String) -> [String: Any?] {
  4509. var dataQuery: [String: Any] = [:]
  4510. Database.shared.database?.inTransaction({ fmdb, rollback in
  4511. if let c = Database().getRecords(fmdb: fmdb, query: "SELECT message_id, f_pin, message_text, attachment_flag, thumb_id, image_id, video_id, file_id FROM MESSAGE where message_id='\(message_id)'"), c.next() {
  4512. dataQuery["message_id"] = c.string(forColumnIndex: 0)
  4513. dataQuery["f_pin"] = c.string(forColumnIndex: 1)
  4514. dataQuery["message_text"] = c.string(forColumnIndex: 2)
  4515. dataQuery["attachment_flag"] = c.string(forColumnIndex: 3)
  4516. dataQuery["thumb_id"] = c.string(forColumnIndex: 4)
  4517. dataQuery["image_id"] = c.string(forColumnIndex: 5)
  4518. dataQuery["video_id"] = c.string(forColumnIndex: 6)
  4519. dataQuery["file_id"] = c.string(forColumnIndex: 7)
  4520. c.close()
  4521. }
  4522. })
  4523. return dataQuery
  4524. }
  4525. @objc func segmentedControlValueChanged(_ sender: segmentedControllerObject) {
  4526. switch sender.selectedSegmentIndex {
  4527. case 0:
  4528. sender.navigation.viewControllers[0].children[1].view.isHidden = true
  4529. break;
  4530. case 1:
  4531. sender.navigation.viewControllers[0].children[1].view.isHidden = false
  4532. break;
  4533. default:
  4534. break;
  4535. }
  4536. }
  4537. private func copyOption(indexPath: IndexPath) -> UIMenu {
  4538. var ratingButtonTitles = ["Text".localized(), "Image".localized()]
  4539. if (dataMessages[indexPath.row]["message_text"] as? String ?? "").isEmpty {
  4540. ratingButtonTitles = ["Image".localized()]
  4541. }
  4542. let dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexPath.section]})
  4543. let copyActions = ratingButtonTitles
  4544. .enumerated()
  4545. .map { index, title in
  4546. return UIAction(
  4547. title: title,
  4548. identifier: nil,
  4549. handler: {(_) in
  4550. if (dataMessages[indexPath.row]["message_text"] as? String ?? "").isEmpty {
  4551. DispatchQueue.main.async {
  4552. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  4553. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  4554. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  4555. if let dirPath = paths.first {
  4556. let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(dataMessages[indexPath.row]["image_id"] as? String ?? "")
  4557. if FileManager.default.fileExists(atPath: imageURL.path) {
  4558. let image = UIImage(contentsOfFile: imageURL.path)
  4559. UIPasteboard.general.image = image
  4560. self.view.makeToast("Image coppied to clipboard".localized(), duration: 3)
  4561. }
  4562. else if FileEncryption.shared.isSecureExists(filename: imageURL.lastPathComponent) {
  4563. do {
  4564. if var imageData = try FileEncryption.shared.readSecure(filename: imageURL.lastPathComponent) {
  4565. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: imageData)
  4566. if dataDecrypt != nil {
  4567. imageData = dataDecrypt!
  4568. }
  4569. let image = UIImage(data: imageData)
  4570. UIPasteboard.general.image = image
  4571. self.view.makeToast("Image coppied to clipboard".localized(), duration: 3)
  4572. }
  4573. } catch {
  4574. }
  4575. }
  4576. }
  4577. }
  4578. return
  4579. }
  4580. if (index == 0) {
  4581. DispatchQueue.main.async {
  4582. UIPasteboard.general.string = dataMessages[indexPath.row]["message_text"] as? String
  4583. self.view.makeToast("Text coppied to clipboard".localized(), duration: 3)
  4584. }
  4585. } else {
  4586. DispatchQueue.main.async {
  4587. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  4588. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  4589. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  4590. if let dirPath = paths.first {
  4591. let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(dataMessages[indexPath.row]["image_id"] as? String ?? "")
  4592. if FileManager.default.fileExists(atPath: imageURL.path) {
  4593. let image = UIImage(contentsOfFile: imageURL.path)
  4594. UIPasteboard.general.image = image
  4595. self.view.makeToast("Image coppied to clipboard".localized(), duration: 3)
  4596. } else if FileEncryption.shared.isSecureExists(filename: imageURL.lastPathComponent) {
  4597. do {
  4598. if var imageData = try FileEncryption.shared.readSecure(filename: imageURL.lastPathComponent) {
  4599. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: imageData)
  4600. if dataDecrypt != nil {
  4601. imageData = dataDecrypt!
  4602. }
  4603. let image = UIImage(data: imageData)
  4604. UIPasteboard.general.image = image
  4605. self.view.makeToast("Image coppied to clipboard".localized(), duration: 3)
  4606. }
  4607. } catch {
  4608. }
  4609. }
  4610. }
  4611. }
  4612. }
  4613. self.dismissKeyboard()
  4614. })
  4615. }
  4616. return UIMenu(
  4617. title: "Copy".localized(),
  4618. image: UIImage(systemName: "doc.on.doc.fill"),
  4619. children: copyActions)
  4620. }
  4621. private func actionDelete(for type: String, title: String, dataMessages: [[String: Any?]]) -> UIAlertAction? {
  4622. return UIAlertAction(title: title, style: .destructive) { [unowned self] _ in
  4623. for i in 0..<dataMessages.count {
  4624. if (type == "me") {
  4625. if let groupingImages = groupImages[dataMessages[i]["message_id"] as? String ?? ""] {
  4626. for i in 0..<groupingImages.count {
  4627. self.deleteMessage(l_pin: dataGroup["group_id"] as? String ?? "", message_id: groupingImages[i].messageId, scope: MessageScope.GROUP, type: "1", chat: dataTopic["chat_id"] as? String ?? "")
  4628. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId })
  4629. if idx != nil {
  4630. self.dataMessages.remove(at: idx!)
  4631. if (idx == self.dataMessages.count - 1) {
  4632. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  4633. }
  4634. for i in 0..<dataDates.count {
  4635. if self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[i] }).count == 0 {
  4636. dataDates.remove(at: i)
  4637. }
  4638. }
  4639. }
  4640. }
  4641. self.groupImages.removeValue(forKey: groupingImages[0].messageId)
  4642. } else {
  4643. if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
  4644. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  4645. imageView.tintColor = .white
  4646. let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  4647. banner.show()
  4648. } else {
  4649. if let groupingImages = groupImages[dataMessages[i]["message_id"] as? String ?? ""] {
  4650. for i in 0..<groupingImages.count {
  4651. self.deleteMessage(l_pin: dataGroup["group_id"] as? String ?? "", message_id: groupingImages[i].messageId, scope: MessageScope.GROUP, type: "2", chat: dataTopic["chat_id"] as? String ?? "")
  4652. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[i].messageId})
  4653. if idx != nil {
  4654. self.dataMessages[idx!]["lock"] = "1"
  4655. self.dataMessages[idx!]["attachment_flag"] = "0"
  4656. self.dataMessages[idx!]["reff_id"] = ""
  4657. }
  4658. }
  4659. if let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == groupingImages[0].messageId}) {
  4660. var dataMessageInGrouping = (groupImages[dataMessages[i]["message_id"] as? String ?? ""]!).map({ $0.dataMessage })
  4661. dataMessageInGrouping.remove(at: 0)
  4662. self.dataMessages.insert(contentsOf: dataMessageInGrouping, at: idx+1)
  4663. self.groupImages.removeValue(forKey: groupingImages[0].messageId)
  4664. }
  4665. } else {
  4666. self.deleteMessage(l_pin: dataGroup["group_id"] as? String ?? "", message_id: dataMessages[i]["message_id"] as? String ?? "", scope: MessageScope.GROUP, type: "1", chat: dataTopic["chat_id"] as? String ?? "")
  4667. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == dataMessages[i]["message_id"] as? String})
  4668. if idx != nil {
  4669. self.dataMessages.remove(at: idx!)
  4670. if (idx == self.dataMessages.count - 1) {
  4671. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  4672. }
  4673. for i in 0..<dataDates.count {
  4674. if self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[i] }).count == 0 {
  4675. dataDates.remove(at: i)
  4676. }
  4677. }
  4678. }
  4679. }
  4680. }
  4681. }
  4682. } else {
  4683. self.deleteMessage(l_pin: dataGroup["group_id"] as? String ?? "", message_id: dataMessages[i]["message_id"] as? String ?? "", scope: MessageScope.GROUP, type: "2", chat: dataTopic["chat_id"] as? String ?? "")
  4684. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == dataMessages[i]["message_id"] as? String ?? ""})
  4685. if idx != nil {
  4686. self.dataMessages[idx!]["lock"] = "1"
  4687. self.dataMessages[idx!]["attachment_flag"] = "0"
  4688. self.dataMessages[idx!]["reff_id"] = ""
  4689. }
  4690. }
  4691. if self.listTimerCredential[dataMessages[i]["message_id"] as? String ?? ""] != nil {
  4692. self.listTimerCredential.removeValue(forKey: dataMessages[i]["message_id"] as? String ?? "")
  4693. self.timerCredential[dataMessages[i]["message_id"] as? String ?? ""]?.invalidate()
  4694. self.timerCredential.removeValue(forKey: dataMessages[i]["message_id"] as? String ?? "")
  4695. }
  4696. }
  4697. let dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  4698. self.pinAllMessages(dataMessages: dataMessagesPin)
  4699. NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
  4700. cancelAction()
  4701. }
  4702. }
  4703. @objc func deleteReplyView() {
  4704. if self.containerPreviewReply.isDescendant(of: self.viewTextfield) {
  4705. self.containerPreviewReply.subviews.forEach { $0.removeFromSuperview() }
  4706. self.containerPreviewReply.removeConstraints(self.containerPreviewReply.constraints)
  4707. self.containerPreviewReply.removeFromSuperview()
  4708. self.reffId = nil
  4709. UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
  4710. self.constraintTopTextField.constant = self.constraintTopTextField.constant - 50 - (self.offset()*3)
  4711. if self.contraintBottomMention.constant > 0 {
  4712. self.contraintBottomMention.constant = self.contraintBottomMention.constant - 50
  4713. }
  4714. }, completion: nil)
  4715. }
  4716. }
  4717. @objc func removeLinkPreviewUntilEmptyTextView() {
  4718. isAlwaysHideLinkPreview = true
  4719. deleteLinkPreview()
  4720. }
  4721. @objc func deleteLinkPreview() {
  4722. if self.containerLink.isDescendant(of: self.viewTextfield) {
  4723. self.containerLink.subviews.forEach { $0.removeFromSuperview() }
  4724. self.containerLink.removeConstraints(self.containerLink.constraints)
  4725. self.containerLink.removeFromSuperview()
  4726. UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
  4727. self.constraintTopTextField.constant = self.constraintTopTextField.constant - 80
  4728. if self.contraintBottomMention.constant > 0 {
  4729. self.contraintBottomMention.constant = self.contraintBottomMention.constant - 80
  4730. }
  4731. }, completion: nil)
  4732. self.showingLink = ""
  4733. }
  4734. if self.reffId != nil {
  4735. self.bottomAnchorPreviewReply.isActive = false
  4736. self.bottomAnchorPreviewReply = self.containerPreviewReply.bottomAnchor.constraint(equalTo: self.textFieldSend.topAnchor)
  4737. self.bottomAnchorPreviewReply.isActive = true
  4738. }
  4739. }
  4740. }
  4741. extension EditorGroup: UICollectionViewDelegate, UICollectionViewDataSource {
  4742. public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  4743. return 76
  4744. }
  4745. public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  4746. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellSticker", for: indexPath)
  4747. if (cell.contentView.subviews.count > 0) {
  4748. cell.contentView.subviews[0].removeFromSuperview()
  4749. }
  4750. let imageSticker = UIImageView()
  4751. cell.contentView.addSubview(imageSticker)
  4752. imageSticker.translatesAutoresizingMaskIntoConstraints = false
  4753. NSLayoutConstraint.activate([
  4754. imageSticker.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
  4755. imageSticker.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
  4756. imageSticker.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
  4757. imageSticker.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)
  4758. ])
  4759. var imageStickerBundle = UIImage(named: stickers[indexPath.row], in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  4760. if imageStickerBundle == nil {
  4761. imageStickerBundle = UIImage(named: stickers[indexPath.row], in: Bundle.resourcesMediaBundle(for: Nexilis.self), with: nil)
  4762. }
  4763. imageSticker.image = imageStickerBundle //resourcesMediaBundle
  4764. return cell
  4765. }
  4766. public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  4767. sendChat(message_text: "sticker/\(stickers[indexPath.row])", attachment_flag: "11", viewController: self)
  4768. constraintBottomAttachment.constant = 0.0
  4769. self.viewSticker.removeConstraints(self.viewSticker.constraints)
  4770. self.viewSticker.removeFromSuperview()
  4771. }
  4772. public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  4773. 1
  4774. }
  4775. public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  4776. return self.previewItem!
  4777. }
  4778. }
  4779. extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayerDelegate {
  4780. // public func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  4781. // checkNewMessage(tableView: tableView)
  4782. // }
  4783. public func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  4784. DispatchQueue.main.asyncAfter(deadline: .now() + 0.6, execute: {
  4785. if self.tableChatView.alpha != 1.0 {
  4786. UIView.animate(withDuration: 0.5, animations: {
  4787. self.tableChatView.alpha = 1.0
  4788. })
  4789. }
  4790. })
  4791. }
  4792. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  4793. lastY = scrollView.contentOffset.y
  4794. DispatchQueue.main.async { [self] in
  4795. if tableChatView.alpha != 1 {
  4796. return
  4797. }
  4798. checkNewMessage(tableView: self.tableChatView)
  4799. }
  4800. }
  4801. public func numberOfSections(in tableView: UITableView) -> Int {
  4802. if tableView == tableMention || tableView == tableMentionEdit || tableView == tableViewConfigFile {
  4803. return 1
  4804. }
  4805. return dataDates.count
  4806. }
  4807. public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  4808. if tableView == tableViewConfigFile {
  4809. return 2
  4810. }
  4811. if tableView == tableMention || tableView == tableMentionEdit {
  4812. return listMentionWithText.count
  4813. }
  4814. let count = dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[section] }).count
  4815. return count
  4816. }
  4817. public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  4818. if tableView == tableViewConfigFile {
  4819. return nil
  4820. }
  4821. if tableView == tableMention || tableView == tableMentionEdit {
  4822. return .none
  4823. }
  4824. let containerView = UIView()
  4825. containerView.backgroundColor = .clear
  4826. let dateView = UIView()
  4827. containerView.addSubview(dateView)
  4828. dateView.translatesAutoresizingMaskIntoConstraints = false
  4829. var topAnchor = dateView.topAnchor.constraint(equalTo: containerView.topAnchor)
  4830. topAnchor = dateView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10)
  4831. NSLayoutConstraint.activate([
  4832. topAnchor,
  4833. dateView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
  4834. dateView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
  4835. dateView.widthAnchor.constraint(greaterThanOrEqualToConstant: 60)
  4836. ])
  4837. dateView.backgroundColor = .orangeColor
  4838. dateView.layer.cornerRadius = 8.0
  4839. dateView.clipsToBounds = true
  4840. let labelDate = UILabel()
  4841. dateView.addSubview(labelDate)
  4842. labelDate.translatesAutoresizingMaskIntoConstraints = false
  4843. NSLayoutConstraint.activate([
  4844. labelDate.centerYAnchor.constraint(equalTo: dateView.centerYAnchor),
  4845. labelDate.centerXAnchor.constraint(equalTo: dateView.centerXAnchor),
  4846. labelDate.leadingAnchor.constraint(equalTo: dateView.leadingAnchor, constant: 10),
  4847. labelDate.trailingAnchor.constraint(equalTo: dateView.trailingAnchor, constant: -10),
  4848. ])
  4849. labelDate.textAlignment = .center
  4850. labelDate.textColor = .secondaryColor
  4851. labelDate.font = UIFont.systemFont(ofSize: 12 + offset(), weight: .medium)
  4852. labelDate.text = dataDates[section]
  4853. return containerView
  4854. }
  4855. public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
  4856. if tableView == tableMention || tableView == tableMentionEdit || tableView == tableViewConfigFile {
  4857. return 0
  4858. }
  4859. return 30
  4860. }
  4861. public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  4862. if tableView == tableViewConfigFile {
  4863. tableView.deselectRow(at: indexPath, animated: true)
  4864. var type = ""
  4865. if indexPath.row == 0 {
  4866. type = "share,download"
  4867. } else {
  4868. type = "forward"
  4869. }
  4870. if !specFileString.contains(type) {
  4871. if !specFileString.isEmpty {
  4872. specFileString += ","
  4873. }
  4874. specFileString += type
  4875. } else {
  4876. specFileString = specFileString.replacingOccurrences(of: type, with: "")
  4877. if specFileString == "," {
  4878. specFileString = ""
  4879. }
  4880. }
  4881. if specFileString.isEmpty {
  4882. buttonSpec.setImage(UIImage(named: "pb_ic_attach_spc_off", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal).resize(target: CGSize(width: 30, height: 30)), for: .normal)
  4883. } else {
  4884. buttonSpec.setImage(UIImage(named: "pb_ic_attach_spc", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal).resize(target: CGSize(width: 30, height: 30)), for: .normal)
  4885. }
  4886. tableView.reloadData()
  4887. return
  4888. }
  4889. if tableView == tableMention || tableView == tableMentionEdit {
  4890. tableView.deselectRow(at: indexPath, animated: true)
  4891. var nowTextField = textFieldSend!
  4892. if tableView == tableMentionEdit {
  4893. nowTextField = editTextView
  4894. }
  4895. let fulltextForMention = nowTextField.text.substring(from: 0, to: lastPositionCursorMention - 1)
  4896. let diff = nowTextField.text.count - fulltextForMention.count
  4897. let lines = fulltextForMention.split(separator: "\n")
  4898. if let lastLineIndex = lines.lastIndex(where: { !$0.isEmpty }) {
  4899. let words = lines[lastLineIndex].split(separator: " ")
  4900. if let lastWordIndex = words.lastIndex(where: { !$0.isEmpty }) {
  4901. var lastWord = words[lastWordIndex]
  4902. if let textM = extractFromAtIfSymbolsBefore(String(lastWord)) {
  4903. lastWord = textM[textM.startIndex..<textM.endIndex]
  4904. }
  4905. if let rangeLastWord = fulltextForMention.range(of: lastWord, options: .backwards) {
  4906. listMentionInTextField.append(listMentionWithText[indexPath.row])
  4907. var addSpaceAfterReplacement = ""
  4908. if diff == 0 {
  4909. addSpaceAfterReplacement = " "
  4910. }
  4911. var text = nowTextField.text ?? ""
  4912. let nameMention = listMentionWithText[indexPath.row].fullName.trimmingCharacters(in: .whitespaces)
  4913. listMentionInTextField.last?.ex_block = "\(fulltextForMention.distance(from: fulltextForMention.startIndex, to: rangeLastWord.lowerBound) + nameMention.count)" //upperbound
  4914. let replacementText = "@\(nameMention)"
  4915. // Replace the old text with the new text using the replaceSubrange(_:with:) method
  4916. text.replaceSubrange(rangeLastWord, with: replacementText + addSpaceAfterReplacement)
  4917. nowTextField.attributedText = text.richText(isEditing: true, group_id: self.dataGroup["group_id"] as? String ?? "", listMentionInTextField: listMentionInTextField)
  4918. let newPosition = nowTextField.position(from: nowTextField.beginningOfDocument, offset: nowTextField.text.count - diff)
  4919. nowTextField.selectedTextRange = nowTextField.textRange(from: newPosition!, to: newPosition!)
  4920. hideMention()
  4921. lastTextLength = nowTextField.text.count
  4922. return
  4923. }
  4924. }
  4925. }
  4926. }
  4927. let dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexPath.section] })
  4928. if copySession || forwardSession || deleteSession {
  4929. guard indexPath.row < dataMessages.count else {
  4930. return
  4931. }
  4932. // if copySession && dataMessages[indexPath.row]["f_pin"] as? String ?? "" != "-999" {
  4933. // return
  4934. // }
  4935. if (dataMessages[indexPath.row]["attachment_flag"] as? String ?? "" != "0" || dataMessages[indexPath.row]["lock"] as? String == "1") && !forwardSession && !deleteSession {
  4936. return
  4937. }
  4938. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == dataMessages[indexPath.row]["message_id"] as? String ?? ""})
  4939. if idx != nil {
  4940. self.dataMessages[idx!]["isSelected"] = !(self.dataMessages[idx!]["isSelected"] as! Bool)
  4941. self.tableChatView.reloadRows(at: [indexPath], with: .none)
  4942. }
  4943. containerMultpileSelectSession.subviews.forEach({ $0.removeFromSuperview() })
  4944. addSubviewMultipleSession()
  4945. return
  4946. }
  4947. if !(dataMessages[indexPath.row]["image_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath.row]["video_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath.row]["file_id"] as? String ?? "").isEmpty || !(dataMessages[indexPath.row]["audio_id"] as? String ?? "").isEmpty {
  4948. if !Nexilis.checkingAccess(key: "secure_folder_forward") && !(dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String ?? "").contains("forward") {
  4949. return
  4950. } else {
  4951. var file = dataMessages[indexPath.row]["image_id"] as? String ?? ""
  4952. if file.isEmpty {
  4953. file = dataMessages[indexPath.row]["video_id"] as? String ?? ""
  4954. if file.isEmpty {
  4955. file = dataMessages[indexPath.row]["file_id"] as? String ?? ""
  4956. if file.isEmpty {
  4957. file = dataMessages[indexPath.row]["audio_id"] as? String ?? ""
  4958. }
  4959. }
  4960. }
  4961. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  4962. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  4963. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  4964. if let dirPath = paths.first {
  4965. let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(file)
  4966. if !FileManager.default.fileExists(atPath: fileURL.path) && !FileEncryption.shared.isSecureExists(filename: fileURL.lastPathComponent) {
  4967. return
  4968. }
  4969. }
  4970. }
  4971. }
  4972. let message = dataMessages[indexPath.row]
  4973. if let attachmentFlag = message["attachment_flag"], let attachmentFlag = attachmentFlag as? String {
  4974. if attachmentFlag == "27" || attachmentFlag == "26" {
  4975. let streamingController = (attachmentFlag == "27") ? QmeraCreateStreamingViewController() : CreateSeminarViewController()
  4976. switch(attachmentFlag){
  4977. case "27":
  4978. (streamingController as! QmeraCreateStreamingViewController).isJoin = true
  4979. default:
  4980. (streamingController as! CreateSeminarViewController).isJoin = true
  4981. }
  4982. if let messageText = message["message_text"],
  4983. let messageText = messageText as? String,
  4984. var json = try! JSONSerialization.jsonObject(with: messageText.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  4985. if json["blog"] == nil {
  4986. json["blog"] = message["blog_id"] ?? nil
  4987. }
  4988. switch(attachmentFlag){
  4989. case "27":
  4990. (streamingController as! QmeraCreateStreamingViewController).data = json
  4991. default:
  4992. (streamingController as! CreateSeminarViewController).data = json
  4993. }
  4994. }
  4995. let streamingNav = CustomNavigationController(rootViewController: streamingController)
  4996. streamingNav.modalPresentationStyle = .custom
  4997. streamingNav.navigationBar.tintColor = .white
  4998. streamingNav.navigationBar.barTintColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
  4999. streamingNav.navigationBar.isTranslucent = false
  5000. streamingNav.navigationBar.overrideUserInterfaceStyle = .dark
  5001. streamingNav.navigationBar.barStyle = .black
  5002. let cancelButtonAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16)]
  5003. UIBarButtonItem.appearance().setTitleTextAttributes(cancelButtonAttributes, for: .normal)
  5004. let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
  5005. streamingNav.navigationBar.titleTextAttributes = textAttributes
  5006. streamingNav.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : .mainColor
  5007. streamingNav.navigationBar.isTranslucent = false
  5008. navigationController?.present(streamingNav, animated: true, completion: nil)
  5009. }
  5010. }
  5011. }
  5012. public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  5013. if tableView == tableViewConfigFile {
  5014. let cell = tableView.dequeueReusableCell(withIdentifier: "cellConfigFile", for: indexPath as IndexPath)
  5015. var content = cell.defaultContentConfiguration()
  5016. content.textProperties.font = .systemFont(ofSize: 16, weight: .medium)
  5017. content.textProperties.color = .label
  5018. content.secondaryTextProperties.font = .systemFont(ofSize: 14)
  5019. content.secondaryTextProperties.color = .gray
  5020. if indexPath.row == 0 {
  5021. content.text = "Can Share and Download".localized()
  5022. content.secondaryText = "The user, as the receiver, can share and download the attachment.".localized()
  5023. cell.accessoryType = specFileString.contains("share,download") ? .checkmark : .none
  5024. } else {
  5025. content.text = "Can Forward".localized()
  5026. content.secondaryText = "The user, as the receiver, can forward the attachment.".localized()
  5027. cell.accessoryType = specFileString.contains("forward") ? .checkmark : .none
  5028. }
  5029. cell.contentConfiguration = content
  5030. cell.tintColor = .black
  5031. return cell
  5032. }
  5033. if tableView == tableMention || tableView == tableMentionEdit {
  5034. let cellMention = tableView.dequeueReusableCell(withIdentifier: tableView == tableMention ? "cellMention" : "cellEditMention", for: indexPath as IndexPath)
  5035. var content = cellMention.defaultContentConfiguration()
  5036. content.textProperties.font = UIFont.systemFont(ofSize: 11 + offset())
  5037. content.imageProperties.tintColor = .black
  5038. content.imageProperties.maximumSize = CGSize(width: 24, height: 24)
  5039. if indexPath.row < listMentionWithText.count {
  5040. getImage(name: listMentionWithText[indexPath.row].thumb, placeholderImage: UIImage(systemName: "person"), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
  5041. content.image = image
  5042. })
  5043. content.text = listMentionWithText[indexPath.row].firstName + " " + listMentionWithText[indexPath.row].lastName
  5044. }
  5045. cellMention.contentConfiguration = content
  5046. return cellMention
  5047. }
  5048. let idMe = User.getMyPin() as String?
  5049. let dataMessages = dataMessages.filter({$0["chat_date"] as? String ?? "" == dataDates[indexPath.section]})
  5050. let cellMessage = tableView.dequeueReusableCell(withIdentifier: "cellEditorGroup", for: indexPath as IndexPath)
  5051. cellMessage.backgroundColor = .clear
  5052. cellMessage.selectionStyle = .none
  5053. cellMessage.contentView.subviews.forEach({ $0.removeConstraints($0.constraints) })
  5054. cellMessage.contentView.subviews.forEach({ $0.removeFromSuperview() })
  5055. let profileMessage = UIImageView()
  5056. profileMessage.frame.size = CGSize(width: 35, height: 35)
  5057. cellMessage.contentView.addSubview(profileMessage)
  5058. profileMessage.translatesAutoresizingMaskIntoConstraints = false
  5059. let tapGestureRecognizer = ObjectGesture(target: self, action: #selector(profilePersonTapped(_:)))
  5060. tapGestureRecognizer.message_id = dataMessages[indexPath.row]["f_pin"] as? String ?? ""
  5061. profileMessage.isUserInteractionEnabled = true
  5062. profileMessage.addGestureRecognizer(tapGestureRecognizer)
  5063. let containerMessage = UIView()
  5064. let messageIdChat = (dataMessages[indexPath.row]["message_id"] as? String) ?? ""
  5065. let thumbChat = dataMessages[indexPath.row]["thumb_id"] as? String ?? ""
  5066. let imageChat = dataMessages[indexPath.row]["image_id"] as? String ?? ""
  5067. let videoChat = dataMessages[indexPath.row]["video_id"] as? String ?? ""
  5068. let fileChat = dataMessages[indexPath.row]["file_id"] as? String ?? ""
  5069. let reffChat = dataMessages[indexPath.row]["reff_id"] as? String ?? ""
  5070. let audioChat = (dataMessages[indexPath.row]["audio_id"] as? String) ?? ""
  5071. let gifChat = (dataMessages[indexPath.row]["gif_id"] as? String) ?? ""
  5072. let dataTimer = listTimerCredential[(dataMessages[indexPath.row]["message_id"] as? String ?? "")]
  5073. cellMessage.contentView.addSubview(containerMessage)
  5074. containerMessage.translatesAutoresizingMaskIntoConstraints = false
  5075. if messageIdChat.contains("NTFPIN_") {
  5076. containerMessage.backgroundColor = .orangeColor
  5077. containerMessage.anchor(top: cellMessage.contentView.topAnchor, bottom: cellMessage.contentView.bottomAnchor, paddingTop: 5, paddingBottom: 5, centerX: cellMessage.contentView.centerXAnchor, minWidth: 40, maxWidth: UIScreen.main.bounds.width - 40)
  5078. containerMessage.layer.cornerRadius = 8
  5079. containerMessage.clipsToBounds = true
  5080. let textMessage = UILabel()
  5081. containerMessage.addSubview(textMessage)
  5082. textMessage.textAlignment = .center
  5083. textMessage.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: 5, paddingLeft: 10, paddingBottom: 5, paddingRight: 10)
  5084. textMessage.font = .systemFont(ofSize: 14)
  5085. textMessage.text = dataMessages[indexPath.row][TypeDataMessage.message_text] as? String ?? ""
  5086. textMessage.textColor = .white
  5087. return cellMessage
  5088. }
  5089. let timeMessage = UILabel()
  5090. timeMessage.numberOfLines = 0
  5091. cellMessage.contentView.addSubview(timeMessage)
  5092. timeMessage.translatesAutoresizingMaskIntoConstraints = false
  5093. if ((dataMessages[indexPath.row]["read_receipts"] as? String) == "8" ||
  5094. (dataMessages[indexPath.row]["credential"] as? String) == "1" ||
  5095. !(dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String ?? "").isEmpty) &&
  5096. (dataMessages[indexPath.row]["lock"] as? String) != "2" &&
  5097. (dataMessages[indexPath.row]["lock"] as? String) != "1" {
  5098. timeMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -40).isActive = true
  5099. } else {
  5100. timeMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -5).isActive = true
  5101. }
  5102. let messageText = UITextView()
  5103. messageText.isEditable = false
  5104. messageText.isSelectable = true
  5105. messageText.dataDetectorTypes = [.link]
  5106. messageText.backgroundColor = .clear
  5107. messageText.isScrollEnabled = false
  5108. messageText.textContainerInset = UIEdgeInsets.zero
  5109. messageText.contentInset = UIEdgeInsets.zero
  5110. messageText.textDragInteraction?.isEnabled = false
  5111. containerMessage.addSubview(messageText)
  5112. messageText.translatesAutoresizingMaskIntoConstraints = false
  5113. var topMarginText = messageText.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 32)
  5114. let dataProfile = getDataProfile(f_pin: dataMessages[indexPath.row]["f_pin"] as? String ?? "", message_id: dataMessages[indexPath.row]["message_id"] as? String ?? "")
  5115. let statusMessage = UIImageView()
  5116. if (dataMessages[indexPath.row]["attachment_flag"] as? String == "0" && dataMessages[indexPath.row]["lock"] as? String != "1") || forwardSession || deleteSession {
  5117. var showSelectedImage = true
  5118. if (!imageChat.isEmpty || !videoChat.isEmpty || !fileChat.isEmpty) && forwardSession {
  5119. if !Nexilis.checkingAccess(key: "secure_folder_forward") && !(dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String ?? "").contains("forward") {
  5120. showSelectedImage = false
  5121. } else {
  5122. var file = dataMessages[indexPath.row]["image_id"] as? String ?? ""
  5123. if file.isEmpty {
  5124. file = dataMessages[indexPath.row]["video_id"] as? String ?? ""
  5125. if file.isEmpty {
  5126. file = dataMessages[indexPath.row]["file_id"] as? String ?? ""
  5127. if file.isEmpty {
  5128. file = dataMessages[indexPath.row]["audio_id"] as? String ?? ""
  5129. }
  5130. }
  5131. }
  5132. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  5133. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  5134. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  5135. if let dirPath = paths.first {
  5136. let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(file)
  5137. if !FileManager.default.fileExists(atPath: fileURL.path) && !FileEncryption.shared.isSecureExists(filename: fileURL.lastPathComponent) {
  5138. showSelectedImage = false
  5139. }
  5140. }
  5141. }
  5142. }
  5143. if dataMessages[indexPath.row]["f_pin"] as? String ?? "" == "-999" && !deleteSession {
  5144. showSelectedImage = false
  5145. }
  5146. if showSelectedImage {
  5147. let selectedImage = UIImageView()
  5148. cellMessage.contentView.addSubview(selectedImage)
  5149. selectedImage.translatesAutoresizingMaskIntoConstraints = false
  5150. selectedImage.frame.size = CGSize(width: 20, height: 20)
  5151. var leading = selectedImage.leadingAnchor.constraint(equalTo: cellMessage.contentView.leadingAnchor, constant: -20)
  5152. selectedImage.isHidden = true
  5153. if copySession || forwardSession || deleteSession {
  5154. leading = selectedImage.leadingAnchor.constraint(equalTo: cellMessage.contentView.leadingAnchor, constant: 15)
  5155. selectedImage.isHidden = false
  5156. }
  5157. NSLayoutConstraint.activate([
  5158. leading,
  5159. selectedImage.centerYAnchor.constraint(equalTo: cellMessage.contentView.centerYAnchor),
  5160. selectedImage.widthAnchor.constraint(equalToConstant: 20),
  5161. selectedImage.heightAnchor.constraint(equalToConstant: 20)
  5162. ])
  5163. selectedImage.circle()
  5164. selectedImage.layer.borderWidth = 2
  5165. selectedImage.layer.borderColor = UIColor.mainColor.cgColor
  5166. if dataMessages[indexPath.row]["isSelected"] as! Bool {
  5167. selectedImage.image = UIImage(systemName: "checkmark.circle.fill")
  5168. }
  5169. selectedImage.tintColor = .mainColor
  5170. }
  5171. }
  5172. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5173. profileMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 5).isActive = true
  5174. profileMessage.trailingAnchor.constraint(equalTo: cellMessage.contentView.trailingAnchor, constant: -15).isActive = true
  5175. profileMessage.heightAnchor.constraint(equalToConstant: 37).isActive = true
  5176. profileMessage.widthAnchor.constraint(equalToConstant: 35).isActive = true
  5177. profileMessage.circle()
  5178. profileMessage.clipsToBounds = true
  5179. profileMessage.backgroundColor = .lightGray
  5180. profileMessage.image = UIImage(systemName: "person")
  5181. profileMessage.tintColor = .white
  5182. profileMessage.contentMode = .scaleAspectFit
  5183. let pictureImage = dataProfile["image_id"]
  5184. if (pictureImage != "" && pictureImage != nil) {
  5185. profileMessage.setImage(name: pictureImage!)
  5186. profileMessage.contentMode = .scaleAspectFill
  5187. }
  5188. containerMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 5).isActive = true
  5189. containerMessage.leadingAnchor.constraint(greaterThanOrEqualTo: cellMessage.contentView.leadingAnchor, constant: 60).isActive = true
  5190. containerMessage.trailingAnchor.constraint(equalTo: profileMessage.leadingAnchor, constant: -5).isActive = true
  5191. containerMessage.widthAnchor.constraint(greaterThanOrEqualToConstant: 46).isActive = true
  5192. containerMessage.layer.cornerRadius = 10.0
  5193. containerMessage.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner, .layerMinXMinYCorner]
  5194. containerMessage.clipsToBounds = true
  5195. timeMessage.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
  5196. if (dataMessages[indexPath.row]["lock"] as? String == "0" || (dataMessages[indexPath.row]["lock"] as? String ?? "").isEmpty) {
  5197. cellMessage.contentView.addSubview(statusMessage)
  5198. statusMessage.translatesAutoresizingMaskIntoConstraints = false
  5199. statusMessage.bottomAnchor.constraint(equalTo: timeMessage.topAnchor).isActive = true
  5200. statusMessage.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
  5201. statusMessage.widthAnchor.constraint(equalToConstant: 15).isActive = true
  5202. statusMessage.heightAnchor.constraint(equalToConstant: 15).isActive = true
  5203. var status = getRealStatus(messageId: dataMessages[indexPath.row]["message_id"] as? String ?? "")
  5204. if status == "-1" {
  5205. status = dataMessages[indexPath.row]["status"]! as? String ?? ""
  5206. }
  5207. if status == "0" {
  5208. statusMessage.image = UIImage(systemName: "xmark.circle")!.withTintColor(UIColor.red, renderingMode: .alwaysOriginal)
  5209. } else if status == "1" {
  5210. statusMessage.image = UIImage(systemName: "clock.arrow.circlepath")!.withTintColor(UIColor.lightGray, renderingMode: .alwaysOriginal)
  5211. } else if status == "2" {
  5212. statusMessage.image = UIImage(named: "checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
  5213. } else if (status == "3") {
  5214. statusMessage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.lightGray)
  5215. } else if (status == "8") {
  5216. statusMessage.image = UIImage(named: "message_status_ack", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
  5217. } else {
  5218. statusMessage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
  5219. }
  5220. }
  5221. let nameSender = UILabel()
  5222. containerMessage.addSubview(nameSender)
  5223. nameSender.translatesAutoresizingMaskIntoConstraints = false
  5224. nameSender.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
  5225. nameSender.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  5226. nameSender.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  5227. nameSender.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  5228. nameSender.text = dataProfile["name"]
  5229. nameSender.textAlignment = .right
  5230. if (dataMessages[indexPath.row]["attachment_flag"] as? String == "11" && dataMessages[indexPath.row]["reff_id"]as? String == "") {
  5231. containerMessage.backgroundColor = .clear
  5232. nameSender.textColor = UIApplication.shared.visibleViewController?.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor
  5233. } else {
  5234. containerMessage.backgroundColor = .blueBubbleColor
  5235. nameSender.textColor = UIApplication.shared.visibleViewController?.traitCollection.userInterfaceStyle == .dark ? .lightGray : .mainColor
  5236. }
  5237. } else {
  5238. if copySession || forwardSession || deleteSession {
  5239. profileMessage.leadingAnchor.constraint(equalTo: cellMessage.contentView.leadingAnchor, constant: 50).isActive = true
  5240. } else {
  5241. profileMessage.leadingAnchor.constraint(equalTo: cellMessage.contentView.leadingAnchor, constant: 15).isActive = true
  5242. }
  5243. profileMessage.heightAnchor.constraint(equalToConstant: 37).isActive = true
  5244. profileMessage.widthAnchor.constraint(equalToConstant: 35).isActive = true
  5245. profileMessage.circle()
  5246. profileMessage.clipsToBounds = true
  5247. profileMessage.backgroundColor = .lightGray
  5248. profileMessage.image = UIImage(systemName: "person")
  5249. profileMessage.tintColor = .white
  5250. profileMessage.contentMode = .scaleAspectFit
  5251. let pictureImage = dataProfile["image_id"]
  5252. if dataMessages[indexPath.row]["f_pin"] as? String == "-999" {
  5253. if !Utils.getIconDock().isEmpty {
  5254. let dataImage = try? Data(contentsOf: URL(string: Utils.getUrlDock()!)!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
  5255. if dataImage != nil {
  5256. profileMessage.image = UIImage(data: dataImage!)
  5257. }
  5258. } else {
  5259. profileMessage.image = UIImage(named: "pb_button", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  5260. }
  5261. profileMessage.contentMode = .scaleAspectFill
  5262. }
  5263. else if (pictureImage != "" && pictureImage != nil) {
  5264. profileMessage.setImage(name: pictureImage!)
  5265. profileMessage.contentMode = .scaleAspectFill
  5266. }
  5267. if markerCounter != nil && dataMessages[indexPath.row]["message_id"] as? String == markerCounter {
  5268. profileMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 35).isActive = true
  5269. containerMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 35).isActive = true
  5270. let newMessagesView = UIView()
  5271. cellMessage.contentView.addSubview(newMessagesView)
  5272. newMessagesView.translatesAutoresizingMaskIntoConstraints = false
  5273. NSLayoutConstraint.activate([
  5274. newMessagesView.topAnchor.constraint(equalTo: newMessagesView.topAnchor),
  5275. newMessagesView.bottomAnchor.constraint(equalTo: containerMessage.topAnchor),
  5276. newMessagesView.centerXAnchor.constraint(equalTo: cellMessage.contentView.centerXAnchor),
  5277. newMessagesView.heightAnchor.constraint(equalToConstant: 30),
  5278. newMessagesView.widthAnchor.constraint(greaterThanOrEqualToConstant: 60)
  5279. ])
  5280. newMessagesView.backgroundColor = .greenColor
  5281. newMessagesView.layer.cornerRadius = 15.0
  5282. newMessagesView.clipsToBounds = true
  5283. let labelNewMessages = UILabel()
  5284. newMessagesView.addSubview(labelNewMessages)
  5285. labelNewMessages.translatesAutoresizingMaskIntoConstraints = false
  5286. NSLayoutConstraint.activate([
  5287. labelNewMessages.centerYAnchor.constraint(equalTo: newMessagesView.centerYAnchor),
  5288. labelNewMessages.centerXAnchor.constraint(equalTo: newMessagesView.centerXAnchor),
  5289. labelNewMessages.leadingAnchor.constraint(equalTo: newMessagesView.leadingAnchor, constant: 10),
  5290. labelNewMessages.trailingAnchor.constraint(equalTo: newMessagesView.trailingAnchor, constant: -10),
  5291. ])
  5292. labelNewMessages.textAlignment = .center
  5293. labelNewMessages.textColor = .secondaryColor
  5294. labelNewMessages.font = UIFont.systemFont(ofSize: 12 + offset(), weight: .medium)
  5295. labelNewMessages.text = "Unread Messages".localized()
  5296. } else {
  5297. profileMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 5).isActive = true
  5298. containerMessage.topAnchor.constraint(equalTo: cellMessage.contentView.topAnchor, constant: 5).isActive = true
  5299. }
  5300. containerMessage.leadingAnchor.constraint(equalTo: profileMessage.trailingAnchor, constant: 5).isActive = true
  5301. containerMessage.trailingAnchor.constraint(lessThanOrEqualTo: cellMessage.contentView.trailingAnchor, constant: -60).isActive = true
  5302. containerMessage.widthAnchor.constraint(greaterThanOrEqualToConstant: 46).isActive = true
  5303. if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" && dataMessages[indexPath.row]["reff_id"]as? String == "" && dataMessages[indexPath.row]["lock"] as? String ?? "" != "1" && dataMessages[indexPath.row]["lock"] as? String != "2" {
  5304. containerMessage.backgroundColor = .clear
  5305. } else {
  5306. containerMessage.backgroundColor = .whiteBubbleColor
  5307. }
  5308. containerMessage.layer.cornerRadius = 10.0
  5309. containerMessage.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner]
  5310. containerMessage.clipsToBounds = true
  5311. timeMessage.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: 8).isActive = true
  5312. let nameSender = UILabel()
  5313. containerMessage.addSubview(nameSender)
  5314. nameSender.translatesAutoresizingMaskIntoConstraints = false
  5315. nameSender.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 15).isActive = true
  5316. nameSender.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  5317. nameSender.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  5318. nameSender.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  5319. if dataMessages[indexPath.row]["f_pin"] as? String == "-999" {
  5320. nameSender.text = "Bot"
  5321. }
  5322. else {
  5323. nameSender.text = dataProfile["name"]
  5324. }
  5325. nameSender.textAlignment = .left
  5326. nameSender.textColor = .mainColor
  5327. }
  5328. if ((dataMessages[indexPath.row]["read_receipts"] as? String) == "8" ||
  5329. (dataMessages[indexPath.row]["credential"] as? String) == "1" ||
  5330. !(dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String ?? "").isEmpty) &&
  5331. (dataMessages[indexPath.row]["lock"] as? String) != "2" &&
  5332. (dataMessages[indexPath.row]["lock"] as? String) != "1" {
  5333. containerMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -40).isActive = true
  5334. } else {
  5335. containerMessage.bottomAnchor.constraint(equalTo: cellMessage.contentView.bottomAnchor, constant: -5).isActive = true
  5336. }
  5337. let imageStared = UIImageView()
  5338. let imageAckView = UIImageView()
  5339. let imageCredentialView = UIImageView()
  5340. let imagePinView = UIImageView()
  5341. if dataMessages[indexPath.row]["is_stared"] as? String == "1" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" == "0") {
  5342. cellMessage.contentView.addSubview(imageStared)
  5343. imageStared.translatesAutoresizingMaskIntoConstraints = false
  5344. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5345. imageStared.bottomAnchor.constraint(equalTo: statusMessage.topAnchor).isActive = true
  5346. imageStared.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
  5347. } else {
  5348. imageStared.bottomAnchor.constraint(equalTo: timeMessage.topAnchor).isActive = true
  5349. imageStared.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: 8).isActive = true
  5350. }
  5351. imageStared.widthAnchor.constraint(equalToConstant: 15).isActive = true
  5352. imageStared.heightAnchor.constraint(equalToConstant: 15).isActive = true
  5353. imageStared.image = UIImage(systemName: "star.fill")
  5354. imageStared.backgroundColor = .clear
  5355. imageStared.tintColor = .systemYellow
  5356. }
  5357. if dataMessages[indexPath.row][TypeDataMessage.is_pinned] as? String != nil && dataMessages[indexPath.row][TypeDataMessage.is_pinned] as? String != "0" {
  5358. cellMessage.contentView.addSubview(imagePinView)
  5359. imagePinView.translatesAutoresizingMaskIntoConstraints = false
  5360. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5361. if imageStared.isDescendant(of: cellMessage.contentView){
  5362. imagePinView.bottomAnchor.constraint(equalTo: imageStared.topAnchor).isActive = true
  5363. } else {
  5364. imagePinView.bottomAnchor.constraint(equalTo: statusMessage.topAnchor).isActive = true
  5365. }
  5366. imagePinView.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: -8).isActive = true
  5367. } else {
  5368. if imageStared.isDescendant(of: cellMessage.contentView){
  5369. imagePinView.bottomAnchor.constraint(equalTo: imageStared.topAnchor).isActive = true
  5370. } else {
  5371. imagePinView.bottomAnchor.constraint(equalTo: timeMessage.topAnchor).isActive = true
  5372. }
  5373. imagePinView.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: 8).isActive = true
  5374. }
  5375. imagePinView.widthAnchor.constraint(equalToConstant: 15).isActive = true
  5376. imagePinView.heightAnchor.constraint(equalToConstant: 15).isActive = true
  5377. imagePinView.image = UIImage(systemName: "pin.fill")
  5378. imagePinView.backgroundColor = .clear
  5379. imagePinView.tintColor = .lightGray
  5380. }
  5381. if dataMessages[indexPath.row]["read_receipts"] as? String == "8" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
  5382. var imageAck = UIImage(named: "ack_icon_gray", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
  5383. cellMessage.contentView.addSubview(imageAckView)
  5384. imageAckView.translatesAutoresizingMaskIntoConstraints = false
  5385. imageAckView.widthAnchor.constraint(equalToConstant: 30).isActive = true
  5386. imageAckView.heightAnchor.constraint(equalToConstant: 30).isActive = true
  5387. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5388. let status = getRealStatus(messageId: dataMessages[indexPath.row]["message_id"] as? String ?? "")
  5389. if status == "8" {
  5390. imageAck = UIImage(named: "ack_icon", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
  5391. }
  5392. imageAckView.topAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: 5).isActive = true
  5393. imageAckView.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 30).isActive = true
  5394. } else {
  5395. let status = dataMessages[indexPath.row]["status"] as? String
  5396. if status == "8" {
  5397. imageAck = UIImage(named: "ack_icon", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
  5398. }
  5399. imageAckView.topAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: 5).isActive = true
  5400. imageAckView.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -30).isActive = true
  5401. let tap = ObjectGesture(target: self, action: #selector(tapAck(_:)))
  5402. tap.indexPath = indexPath
  5403. imageAckView.addGestureRecognizer(tap)
  5404. imageAckView.isUserInteractionEnabled = true
  5405. }
  5406. imageAckView.image = imageAck
  5407. }
  5408. if (dataMessages[indexPath.row]["credential"] as? String) == "1" && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
  5409. let imageCredential = UIImage(named: "confidential_icon", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
  5410. imageCredentialView.image = imageCredential
  5411. cellMessage.contentView.addSubview(imageCredentialView)
  5412. imageCredentialView.translatesAutoresizingMaskIntoConstraints = false
  5413. imageCredentialView.widthAnchor.constraint(equalToConstant: 30).isActive = true
  5414. imageCredentialView.heightAnchor.constraint(equalToConstant: 30).isActive = true
  5415. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5416. imageCredentialView.topAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: 5).isActive = true
  5417. imageCredentialView.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 30).isActive = true
  5418. } else {
  5419. imageCredentialView.topAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: 5).isActive = true
  5420. imageCredentialView.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -30).isActive = true
  5421. }
  5422. }
  5423. if !(dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String ?? "").isEmpty && (dataMessages[indexPath.row]["lock"] as? String) != "2" && (dataMessages[indexPath.row]["lock"] as? String) != "1" {
  5424. let imageSpecFileView = UIImageView()
  5425. let imageSpecFile = UIImage(named: "pb_ic_attach_spc", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withRenderingMode(.alwaysOriginal)
  5426. imageSpecFileView.image = imageSpecFile
  5427. cellMessage.contentView.addSubview(imageSpecFileView)
  5428. imageSpecFileView.translatesAutoresizingMaskIntoConstraints = false
  5429. imageSpecFileView.widthAnchor.constraint(equalToConstant: 30).isActive = true
  5430. imageSpecFileView.heightAnchor.constraint(equalToConstant: 30).isActive = true
  5431. imageSpecFileView.topAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: 5).isActive = true
  5432. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5433. if imageAckView.isDescendant(of: cellMessage.contentView) {
  5434. imageSpecFileView.leadingAnchor.constraint(equalTo: imageAckView.trailingAnchor, constant: 5).isActive = true
  5435. } else if imageCredentialView.isDescendant(of: cellMessage.contentView) {
  5436. imageSpecFileView.leadingAnchor.constraint(equalTo: imageCredentialView.trailingAnchor, constant: 5).isActive = true
  5437. } else {
  5438. imageSpecFileView.trailingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 30).isActive = true
  5439. }
  5440. } else {
  5441. if imageAckView.isDescendant(of: cellMessage.contentView) {
  5442. imageSpecFileView.trailingAnchor.constraint(equalTo: imageAckView.leadingAnchor, constant: -5).isActive = true
  5443. } else if imageCredentialView.isDescendant(of: cellMessage.contentView) {
  5444. imageSpecFileView.trailingAnchor.constraint(equalTo: imageCredentialView.leadingAnchor, constant: -5).isActive = true
  5445. } else {
  5446. imageSpecFileView.leadingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -30).isActive = true
  5447. }
  5448. }
  5449. }
  5450. if dataMessages[indexPath.row]["attachment_flag"] as? String ?? "" == "27" || dataMessages[indexPath.row]["attachment_flag"] as? String ?? "" == "26" {
  5451. messageText.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 85).isActive = true
  5452. let imageLS = UIImageView()
  5453. containerMessage.addSubview(imageLS)
  5454. imageLS.translatesAutoresizingMaskIntoConstraints = false
  5455. NSLayoutConstraint.activate([
  5456. imageLS.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15.0),
  5457. imageLS.trailingAnchor.constraint(equalTo: messageText.leadingAnchor, constant: -10.0),
  5458. imageLS.centerYAnchor.constraint(equalTo: containerMessage.centerYAnchor),
  5459. imageLS.heightAnchor.constraint(equalToConstant: 60.0)
  5460. ])
  5461. if dataMessages[indexPath.row]["attachment_flag"] as? String ?? "" == "26" {
  5462. imageLS.image = UIImage(named: "pb_seminar_wpr", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  5463. } else if dataMessages[indexPath.row]["attachment_flag"] as? String ?? "" == "27" {
  5464. imageLS.image = UIImage(named: "pb_live_tv", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  5465. }
  5466. } else if !audioChat.isEmpty {
  5467. messageText.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 60).isActive = true
  5468. } else {
  5469. messageText.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  5470. }
  5471. messageText.bottomAnchor.constraint(equalTo: containerMessage.bottomAnchor, constant: -15).isActive = true
  5472. messageText.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  5473. messageText.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  5474. messageText.font = .systemFont(ofSize: 12 + offset())
  5475. var textChat = dataMessages[indexPath.row]["message_text"] as? String ?? ""
  5476. let originalMessageText = textChat
  5477. if (dataMessages[indexPath.row]["lock"] != nil && (dataMessages[indexPath.row]["lock"])! as? String == "1") {
  5478. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5479. textChat = "🚫 _"+"You were deleted this message".localized()+"_"
  5480. } else {
  5481. textChat = "🚫 _"+"This message was deleted".localized()+"_"
  5482. }
  5483. }
  5484. if dataMessages[indexPath.row]["lock"] as? String == "2" {
  5485. textChat = "🚫 _"+"Message has expired".localized()+"_"
  5486. }
  5487. if !audioChat.isEmpty {
  5488. textChat = textChat.components(separatedBy: "|")[0]
  5489. }
  5490. let imageSticker = UIImageView()
  5491. if let attachmentFlag = dataMessages[indexPath.row]["attachment_flag"], let attachmentFlag = attachmentFlag as? String {
  5492. if attachmentFlag == "27" || attachmentFlag == "26" { // live streaming
  5493. if let json = try! JSONSerialization.jsonObject(with: textChat.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  5494. Database.shared.database?.inTransaction({ fmdb, rollback in
  5495. let title = json["title"] as? String ?? ""
  5496. let description = json["description"] as? String ?? ""
  5497. let start = json["time"] as! Int64
  5498. let by = json["by"] as? String ?? ""
  5499. let textLS = "Live Streaming".localized()
  5500. var type = "*\(textLS)*"
  5501. if attachmentFlag == "26" {
  5502. let textSeminar = "Seminar".localized()
  5503. type = "*\(textSeminar)*"
  5504. }
  5505. if let c = Database().getRecords(fmdb: fmdb, query: "select first_name || ' ' || last_name from BUDDY where f_pin = '\(by)'"), c.next() {
  5506. let name = c.string(forColumnIndex: 0)!
  5507. messageText.attributedText = "\(type) \nTitle: \(title) \nDescription: \(description) \nStart: \(Date(milliseconds: start).format(dateFormat: "dd/MM/yyyy HH:mm")) \nBroadcaster: \(name)".richText()
  5508. c.close()
  5509. } else {
  5510. messageText.attributedText = ("\(type) \nTitle: \(title) \nDescription: \(description) \nStart: \(Date(milliseconds: start).format(dateFormat: "dd/MM/yyyy HH:mm")) \nBroadcaster: " + "Unknown".localized()).richText()
  5511. }
  5512. })
  5513. }
  5514. }
  5515. else if attachmentFlag == "11" && dataMessages[indexPath.row]["lock"] as? String ?? "" != "1" && dataMessages[indexPath.row]["lock"] as? String != "2" {
  5516. messageText.text = ""
  5517. topMarginText.constant = topMarginText.constant + 100
  5518. containerMessage.addSubview(imageSticker)
  5519. imageSticker.translatesAutoresizingMaskIntoConstraints = false
  5520. let data = queryMessageReply(message_id: reffChat)
  5521. if reffChat.isEmpty || data.count == 0 {
  5522. imageSticker.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 32).isActive = true
  5523. imageSticker.widthAnchor.constraint(equalToConstant: 80).isActive = true
  5524. } else {
  5525. imageSticker.widthAnchor.constraint(greaterThanOrEqualToConstant: 80).isActive = true
  5526. }
  5527. imageSticker.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  5528. imageSticker.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
  5529. imageSticker.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  5530. var imageStickerBundle = UIImage(named: (textChat.components(separatedBy: "/")[1]), in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
  5531. if imageStickerBundle == nil {
  5532. imageStickerBundle = UIImage(named: (textChat.components(separatedBy: "/")[1]), in: Bundle.resourcesMediaBundle(for: Nexilis.self), with: nil)
  5533. }
  5534. imageSticker.image = imageStickerBundle //resourcesMediaBundle
  5535. imageSticker.contentMode = .scaleAspectFit
  5536. }
  5537. else {
  5538. messageText.attributedText = textChat.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  5539. modifyText()
  5540. }
  5541. } else {
  5542. messageText.attributedText = textChat.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  5543. modifyText()
  5544. }
  5545. func modifyText() {
  5546. if !textChat.isEmpty {
  5547. if textChat.contains("■"){
  5548. textChat = textChat.components(separatedBy: "■")[0]
  5549. textChat = textChat.trimmingCharacters(in: .whitespacesAndNewlines)
  5550. }
  5551. if !fileChat.isEmpty && dataMessages[indexPath.row]["lock"] as? String != "1" && dataMessages[indexPath.row]["lock"] as? String != "2" {
  5552. textChat = textChat.components(separatedBy: "|")[1]
  5553. }
  5554. let finalAtribute = textChat.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  5555. textChat = finalAtribute.string
  5556. let urlPattern = "(https?://|www\\.)\\S+"
  5557. if let regex = try? NSRegularExpression(pattern: urlPattern, options: []) {
  5558. let matches = regex.matches(in: textChat, options: [], range: NSRange(textChat.startIndex..., in: textChat))
  5559. for match in matches {
  5560. if let range = Range(match.range, in: textChat) {
  5561. let linkText = String(textChat[range])
  5562. let nsRange = NSRange(range, in: textChat)
  5563. finalAtribute.addAttribute(.link, value: linkText, range: nsRange)
  5564. finalAtribute.addAttribute(.foregroundColor, value: UIColor.blue, range: nsRange)
  5565. finalAtribute.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: nsRange)
  5566. }
  5567. }
  5568. }
  5569. messageText.attributedText = finalAtribute
  5570. messageText.delegate = self
  5571. }
  5572. }
  5573. if !copySession && !forwardSession && !deleteSession && !isHistoryCC && !removed {
  5574. let interaction = UIContextMenuInteraction(delegate: self)
  5575. containerMessage.addInteraction(interaction)
  5576. containerMessage.isUserInteractionEnabled = true
  5577. }
  5578. if isSearching && textSearch.count > 1 {
  5579. messageText.attributedText = textChat.richText(isSearching: true, textSearch: textSearch, group_id: self.dataGroup["group_id"] as? String ?? "")
  5580. }
  5581. let stringDate = (dataMessages[indexPath.row]["server_date"] as? String ?? "")
  5582. if !stringDate.isEmpty {
  5583. if (dataMessages[indexPath.row]["credential"] as? String) == "1" && dataMessages[indexPath.row]["lock"] as? String != "2" && dataMessages[indexPath.row]["lock"] as? String != "1" {
  5584. if dataTimer! >= 10 {
  5585. timeMessage.text = "00:\(dataTimer!)"
  5586. } else {
  5587. timeMessage.text = "00:0\(dataTimer!)"
  5588. }
  5589. timeMessage.textColor = .systemRed
  5590. } else {
  5591. let date = Date(milliseconds: Int64(stringDate) ?? 100)
  5592. let formatter = DateFormatter()
  5593. formatter.dateFormat = "HH:mm"
  5594. formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
  5595. timeMessage.text = formatter.string(from: date as Date)
  5596. timeMessage.textColor = .lightGray
  5597. }
  5598. timeMessage.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
  5599. if dataMessages[indexPath.row][TypeDataMessage.last_edit] != nil && dataMessages[indexPath.row][TypeDataMessage.last_edit] as! Int64 != 0 {
  5600. timeMessage.text = (timeMessage.text ?? "") + "\n" + "Edited".localized()
  5601. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5602. timeMessage.textAlignment = .right
  5603. }
  5604. }
  5605. }
  5606. let imageThumb = UIImageView()
  5607. let containerViewFile = UIView()
  5608. let imageGif = SDAnimatedImageView()
  5609. if !audioChat.isEmpty {
  5610. messageText.isHidden = true
  5611. let imageAudio = UIImageView()
  5612. imageAudio.image = UIImage(systemName: "music.note", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35))
  5613. containerMessage.addSubview(imageAudio)
  5614. imageAudio.anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, paddingTop: 15, paddingLeft: 15, paddingBottom: 15, centerY: containerMessage.centerYAnchor)
  5615. imageAudio.tintColor = .mainColor
  5616. let playButtonAudio = UIButton(type: .system)
  5617. playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
  5618. playButtonAudio.tintColor = .gray
  5619. containerMessage.addSubview(playButtonAudio)
  5620. playButtonAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 60, centerY: containerMessage.centerYAnchor, width: 20, height: 20)
  5621. let progressSliderAudio = UISlider()
  5622. progressSliderAudio.minimumValue = 0
  5623. progressSliderAudio.maximumValue = 1
  5624. let thumbImage = UIImage(systemName: "circle.fill")?.withTintColor(UIColor.mainColor)
  5625. .resize(target: CGSize(width: 15, height: 15))
  5626. progressSliderAudio.setThumbImage(thumbImage, for: .normal)
  5627. containerMessage.addSubview(progressSliderAudio)
  5628. progressSliderAudio.anchor(left: playButtonAudio.rightAnchor, right: containerMessage.rightAnchor, paddingLeft: 10, paddingRight: 15, centerY: containerMessage.centerYAnchor, height: 15)
  5629. let timeLabelAudio = UILabel()
  5630. timeLabelAudio.text = "0:00"
  5631. timeLabelAudio.font = .systemFont(ofSize: 10 + offset())
  5632. timeLabelAudio.textColor = .gray
  5633. containerMessage.addSubview(timeLabelAudio)
  5634. timeLabelAudio.anchor(top: playButtonAudio.bottomAnchor, left: playButtonAudio.rightAnchor, paddingLeft: 10, width: 100, height: 12)
  5635. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  5636. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  5637. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  5638. if let dirPath = paths.first {
  5639. let audioURL = URL(fileURLWithPath: dirPath).appendingPathComponent(audioChat)
  5640. var url = audioURL
  5641. if !FileManager.default.fileExists(atPath: audioURL.path) && !FileEncryption.shared.isSecureExists(filename: audioChat) {
  5642. let activityIndicator = UIActivityIndicatorView(style: .medium)
  5643. activityIndicator.translatesAutoresizingMaskIntoConstraints = false
  5644. activityIndicator.startAnimating()
  5645. playButtonAudio.setImage(nil, for: .normal)
  5646. playButtonAudio.addSubview(activityIndicator)
  5647. NSLayoutConstraint.activate([
  5648. activityIndicator.centerXAnchor.constraint(equalTo: playButtonAudio.centerXAnchor),
  5649. activityIndicator.centerYAnchor.constraint(equalTo: playButtonAudio.centerYAnchor)
  5650. ])
  5651. Download().startHTTP(forKey: audioChat) { (name, progress) in
  5652. guard progress == 100 else {
  5653. return
  5654. }
  5655. tableView.reloadRows(at: [indexPath], with: .none)
  5656. }
  5657. } else {
  5658. if !FileManager.default.fileExists(atPath: audioURL.path) {
  5659. do {
  5660. if var audioData = try FileEncryption.shared.readSecure(filename: audioChat) {
  5661. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: audioData)
  5662. if dataDecrypt != nil {
  5663. audioData = dataDecrypt!
  5664. }
  5665. let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
  5666. let tempPath = cachesDirectory.appendingPathComponent(audioChat)
  5667. try audioData.write(to: tempPath)
  5668. url = tempPath
  5669. }
  5670. } catch {
  5671. }
  5672. }
  5673. if audioPlayers[indexPath] == nil {
  5674. do {
  5675. let audioPlayer = try AVAudioPlayer(contentsOf: url)
  5676. audioPlayers[indexPath] = audioPlayer
  5677. audioPlayer.delegate = self
  5678. progressSliderAudio.maximumValue = Float(audioPlayer.duration)
  5679. timeLabelAudio.text = formatTime(audioPlayer.duration)
  5680. } catch {
  5681. print("Error loading audio: \(error)")
  5682. }
  5683. }
  5684. let audioPlayer = audioPlayers[indexPath]
  5685. if playingIndexPath == indexPath, let player = audioPlayer, player.isPlaying {
  5686. playButtonAudio.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  5687. } else {
  5688. playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
  5689. }
  5690. // Play/Pause Button Action
  5691. playButtonAudio.addAction(UIAction { _ in
  5692. self.playPauseAudio(indexPath: indexPath, playButton: playButtonAudio, progressSlider: progressSliderAudio, timeLabel: timeLabelAudio)
  5693. }, for: .touchUpInside)
  5694. progressSliderAudio.addAction(UIAction { _ in
  5695. self.sliderChanged(indexPath: indexPath, progressSlider: progressSliderAudio, timeLabel: timeLabelAudio)
  5696. }, for: .valueChanged)
  5697. }
  5698. }
  5699. }
  5700. if (!thumbChat.isEmpty && dataMessages[indexPath.row]["lock"] as? String ?? "" != "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
  5701. if let listImages = groupImages[messageIdChat] {
  5702. timeMessage.isHidden = true
  5703. statusMessage.isHidden = true
  5704. imageStared.isHidden = true
  5705. topMarginText.constant = topMarginText.constant + 220
  5706. var constTop = 35.0
  5707. if dataMessages[indexPath.row][TypeDataMessage.is_forwarded] != nil && dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int != 0 {
  5708. topMarginText.constant = topMarginText.constant + 20
  5709. constTop = 55.0
  5710. }
  5711. let listImageThumb: [UIImageView] = [UIImageView(), UIImageView(), UIImageView(), UIImageView()]
  5712. for i in 0..<4 {
  5713. containerMessage.addSubview(listImageThumb[i])
  5714. listImageThumb[i].layer.cornerRadius = 5.0
  5715. listImageThumb[i].clipsToBounds = true
  5716. listImageThumb[i].contentMode = .scaleAspectFill
  5717. let widthHeightImage: CGFloat = 120
  5718. switch i {
  5719. case 0:
  5720. listImageThumb[i].anchor(top: containerMessage.topAnchor, left: containerMessage.leftAnchor, paddingTop: constTop, paddingLeft: 5, width: widthHeightImage, height: widthHeightImage)
  5721. case 1:
  5722. listImageThumb[i].anchor(top: containerMessage.topAnchor, left: listImageThumb[0].rightAnchor, right: containerMessage.rightAnchor, paddingTop: constTop, paddingLeft: 5, paddingRight: 5, width: widthHeightImage, height: widthHeightImage)
  5723. case 2:
  5724. listImageThumb[i].anchor(left: containerMessage.leftAnchor, bottom: containerMessage.bottomAnchor, paddingLeft: 5, paddingBottom: 5, width: widthHeightImage, height: widthHeightImage)
  5725. default:
  5726. listImageThumb[i].anchor(left: listImageThumb[2].rightAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, width: widthHeightImage, height: widthHeightImage)
  5727. }
  5728. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  5729. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  5730. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  5731. if let dirPath = paths.first {
  5732. let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(listImages[i].thumbId)
  5733. if FileManager.default.fileExists(atPath: thumbURL.path) {
  5734. DispatchQueue.main.async {
  5735. let image : UIImage? = {
  5736. if let img = Nexilis.imageCache.object(forKey: listImages[i].thumbId as NSString) {
  5737. return img
  5738. }
  5739. else if let img = UIImage(contentsOfFile: thumbURL.path)?.resize(target: CGSize(width: 500, height: 500)) {
  5740. Nexilis.imageCache.setObject(img, forKey: listImages[i].thumbId as NSString)
  5741. return img
  5742. }
  5743. return nil
  5744. }()
  5745. listImageThumb[i].image = image
  5746. }
  5747. } else if FileEncryption.shared.isSecureExists(filename: listImages[i].thumbId) {
  5748. do {
  5749. if var data = try FileEncryption.shared.readSecure(filename: listImages[i].thumbId) {
  5750. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
  5751. if dataDecrypt != nil {
  5752. data = dataDecrypt!
  5753. }
  5754. DispatchQueue.main.async {
  5755. let image : UIImage? = {
  5756. if let img = Nexilis.imageCache.object(forKey: listImages[i].thumbId as NSString) {
  5757. return img
  5758. }
  5759. else if let img = UIImage(data: data)?.resize(target: CGSize(width: 500, height: 500)) {
  5760. Nexilis.imageCache.setObject(img, forKey: listImages[i].thumbId as NSString)
  5761. return img
  5762. }
  5763. return nil
  5764. }()
  5765. listImageThumb[i].image = image
  5766. }
  5767. }
  5768. } catch {
  5769. }
  5770. } else {
  5771. Download().startHTTP(forKey: listImages[i].thumbId) { (name, progress) in
  5772. guard progress == 100 else {
  5773. return
  5774. }
  5775. tableView.reloadRows(at: [indexPath], with: .none)
  5776. }
  5777. }
  5778. let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(listImages[i].imageId)
  5779. if !FileManager.default.fileExists(atPath: imageURL.path) && !FileEncryption.shared.isSecureExists(filename: imageURL.lastPathComponent) {
  5780. let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
  5781. let blurEffectView = UIVisualEffectView(effect: blurEffect)
  5782. blurEffectView.frame = CGRect(x: 0, y: 0, width: listImageThumb[i].frame.size.width, height: listImageThumb[i].frame.size.height)
  5783. blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  5784. listImageThumb[i].addSubview(blurEffectView)
  5785. }
  5786. }
  5787. let containerTimeStatus = UIView()
  5788. listImageThumb[i].addSubview(containerTimeStatus)
  5789. containerTimeStatus.anchor(bottom: listImageThumb[i].bottomAnchor, right: listImageThumb[i].rightAnchor, height: 15)
  5790. let widthcontainerTimeStatus = containerTimeStatus.widthAnchor.constraint(equalToConstant: 50)
  5791. widthcontainerTimeStatus.isActive = true
  5792. containerTimeStatus.layer.cornerRadius = 5.0
  5793. containerTimeStatus.layer.masksToBounds = true
  5794. containerTimeStatus.backgroundColor = .black.withAlphaComponent(0.15)
  5795. let timeInImage = UILabel()
  5796. containerTimeStatus.addSubview(timeInImage)
  5797. let date = Date(milliseconds: Int64(listImages[i].time) ?? 100)
  5798. let formatter = DateFormatter()
  5799. formatter.dateFormat = "HH:mm"
  5800. formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
  5801. timeInImage.text = formatter.string(from: date as Date)
  5802. timeInImage.textColor = .white
  5803. timeInImage.font = UIFont.systemFont(ofSize: 10 + offset(), weight: .medium)
  5804. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5805. let statusInImage = UIImageView()
  5806. containerTimeStatus.addSubview(statusInImage)
  5807. statusInImage.anchor(right: containerTimeStatus.rightAnchor, centerY: containerTimeStatus.centerYAnchor, width: 15, height: 15)
  5808. if listImages[i].status == "0" {
  5809. statusMessage.image = UIImage(systemName: "xmark.circle")!.withTintColor(UIColor.red, renderingMode: .alwaysOriginal)
  5810. } else if listImages[i].status == "1" {
  5811. statusMessage.image = UIImage(systemName: "clock.arrow.circlepath")!.withTintColor(UIColor.lightGray, renderingMode: .alwaysOriginal)
  5812. } else if listImages[i].status == "2" {
  5813. statusInImage.image = UIImage(named: "checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.white)
  5814. } else if listImages[i].status == "3" {
  5815. statusInImage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.white)
  5816. } else {
  5817. statusInImage.image = UIImage(named: "double-checklist", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!.withTintColor(UIColor.systemBlue)
  5818. }
  5819. timeInImage.anchor(right: statusInImage.leftAnchor, centerY: containerTimeStatus.centerYAnchor, height: 15)
  5820. } else {
  5821. timeInImage.anchor(right: containerTimeStatus.rightAnchor, paddingRight: 5, centerY: containerTimeStatus.centerYAnchor, height: 15)
  5822. widthcontainerTimeStatus.constant = widthcontainerTimeStatus.constant - 10
  5823. }
  5824. if listImages[i].dataMessage["is_stared"] as? String == "1" {
  5825. let iconStar = UIImageView()
  5826. containerTimeStatus.addSubview(iconStar)
  5827. iconStar.anchor(right: timeInImage.leftAnchor, paddingRight: 2, centerY: containerTimeStatus.centerYAnchor, width: 15, height: 15)
  5828. widthcontainerTimeStatus.constant = widthcontainerTimeStatus.constant + 15
  5829. iconStar.image = UIImage(systemName: "star.fill")
  5830. iconStar.tintColor = .white
  5831. }
  5832. if !copySession && !forwardSession && !deleteSession {
  5833. let objectTap = ObjectGesture(target: self, action: #selector(imageGroupingTapped(_:)))
  5834. listImageThumb[i].isUserInteractionEnabled = true
  5835. listImageThumb[i].addGestureRecognizer(objectTap)
  5836. objectTap.indexImageTapped = i
  5837. objectTap.listImageFromGrouping = listImages
  5838. objectTap.isInitiator = dataMessages[indexPath.row]["f_pin"] as? String == idMe
  5839. }
  5840. }
  5841. if listImages.count > 4 {
  5842. let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.dark)
  5843. let blurEffectView = UIVisualEffectView(effect: blurEffect)
  5844. blurEffectView.frame = CGRect(x: 0, y: 0, width: listImageThumb[3].frame.size.width, height: listImageThumb[3].frame.size.height)
  5845. blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  5846. listImageThumb[3].addSubview(blurEffectView)
  5847. let countRestImages = UILabel()
  5848. listImageThumb[3].addSubview(countRestImages)
  5849. countRestImages.anchor(centerX: listImageThumb[3].centerXAnchor, centerY: listImageThumb[3].centerYAnchor)
  5850. countRestImages.font = UIFont.systemFont(ofSize: 30, weight: .medium)
  5851. countRestImages.text = "+\(listImages.count - 3)"
  5852. countRestImages.textColor = .white
  5853. }
  5854. } else {
  5855. let getHeightImage = ListGroupImages.getImageSize(image: thumbChat, screenWidth: self.view.frame.size.width * 0.6, screenHeight: 305)!.height
  5856. let getWidthImage = ListGroupImages.getImageSize(image: thumbChat, screenWidth: self.view.frame.size.width * 0.6, screenHeight: 305)!.width
  5857. topMarginText.constant = topMarginText.constant + (getHeightImage < 40 ? 40 : getHeightImage)
  5858. containerMessage.addSubview(imageThumb)
  5859. imageThumb.translatesAutoresizingMaskIntoConstraints = false
  5860. imageThumb.frame = CGRect(x: 0, y: 0, width: getWidthImage, height: getHeightImage)
  5861. let data = queryMessageReply(message_id: reffChat)
  5862. if (reffChat.isEmpty || data.count == 0) && (dataMessages[indexPath.row][TypeDataMessage.is_forwarded] == nil || dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int == 0) {
  5863. imageThumb.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 37).isActive = true
  5864. }
  5865. imageThumb.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  5866. imageThumb.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
  5867. imageThumb.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  5868. imageThumb.widthAnchor.constraint(equalToConstant: getWidthImage).isActive = true
  5869. imageThumb.layer.cornerRadius = 5.0
  5870. imageThumb.clipsToBounds = true
  5871. imageThumb.contentMode = .scaleAspectFill
  5872. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  5873. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  5874. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  5875. if let dirPath = paths.first {
  5876. let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumbChat)
  5877. if FileManager.default.fileExists(atPath: thumbURL.path) {
  5878. DispatchQueue.main.async {
  5879. let image : UIImage? = {
  5880. if let img = Nexilis.imageCache.object(forKey: thumbChat as NSString) {
  5881. return img
  5882. }
  5883. else if let img = UIImage(contentsOfFile: thumbURL.path)?.resize(target: CGSize(width: 500, height: 500)) {
  5884. Nexilis.imageCache.setObject(img, forKey: thumbChat as NSString)
  5885. return img
  5886. }
  5887. return nil
  5888. }()
  5889. imageThumb.image = image
  5890. }
  5891. } else if FileEncryption.shared.isSecureExists(filename: thumbChat) {
  5892. do {
  5893. if var data = try FileEncryption.shared.readSecure(filename: thumbChat) {
  5894. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
  5895. if dataDecrypt != nil {
  5896. data = dataDecrypt!
  5897. }
  5898. DispatchQueue.main.async {
  5899. let image : UIImage? = {
  5900. if let img = Nexilis.imageCache.object(forKey: thumbChat as NSString) {
  5901. return img
  5902. }
  5903. else if let img = UIImage(data: data)?.resize(target: CGSize(width: 500, height: 500)) {
  5904. Nexilis.imageCache.setObject(img, forKey: thumbChat as NSString)
  5905. return img
  5906. }
  5907. return nil
  5908. }()
  5909. imageThumb.image = image
  5910. }
  5911. }
  5912. } catch {
  5913. }
  5914. } else {
  5915. Download().startHTTP(forKey: thumbChat) { (name, progress) in
  5916. guard progress == 100 else {
  5917. return
  5918. }
  5919. tableView.reloadRows(at: [indexPath], with: .none)
  5920. }
  5921. }
  5922. let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(imageChat)
  5923. if !FileManager.default.fileExists(atPath: imageURL.path) && !FileEncryption.shared.isSecureExists(filename: imageURL.lastPathComponent) {
  5924. let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.light)
  5925. let blurEffectView = UIVisualEffectView(effect: blurEffect)
  5926. blurEffectView.frame = CGRect(x: 0, y: 0, width: imageThumb.frame.size.width, height: imageThumb.frame.size.height)
  5927. blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  5928. imageThumb.addSubview(blurEffectView)
  5929. if !imageChat.isEmpty {
  5930. let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 50, weight: .bold, scale: .default)))
  5931. imageThumb.addSubview(blurEffectView)
  5932. imageThumb.addSubview(imageDownload)
  5933. imageDownload.tintColor = .black.withAlphaComponent(0.3)
  5934. imageDownload.translatesAutoresizingMaskIntoConstraints = false
  5935. imageDownload.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
  5936. imageDownload.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
  5937. }
  5938. }
  5939. }
  5940. if (videoChat != "" && gifChat.isEmpty) {
  5941. let imagePlay = UIImageView(image: UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .bold, scale: .default))?.imageWithInsets(insets: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))?.withTintColor(.white))
  5942. imagePlay.circle()
  5943. imageThumb.addSubview(imagePlay)
  5944. imagePlay.backgroundColor = .black.withAlphaComponent(0.3)
  5945. imagePlay.translatesAutoresizingMaskIntoConstraints = false
  5946. imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
  5947. imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
  5948. } else if !gifChat.isEmpty {
  5949. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  5950. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  5951. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  5952. if let dirPath = paths.first {
  5953. let gifURL = URL(fileURLWithPath: dirPath).appendingPathComponent(gifChat)
  5954. if !FileManager.default.fileExists(atPath: gifURL.path) && !FileEncryption.shared.isSecureExists(filename: gifChat) {
  5955. Download().startHTTP(forKey: gifChat) { (name, progress) in
  5956. guard progress == 100 else {
  5957. return
  5958. }
  5959. tableView.reloadRows(at: [indexPath], with: .none)
  5960. }
  5961. } else {
  5962. imageThumb.addSubview(imageGif)
  5963. imageGif.translatesAutoresizingMaskIntoConstraints = false
  5964. imageGif.anchor(top: imageThumb.topAnchor, left: imageThumb.leftAnchor, bottom: imageThumb.bottomAnchor, right: imageThumb.rightAnchor)
  5965. if FileManager.default.fileExists(atPath: gifURL.path) {
  5966. imageGif.image = SDAnimatedImage(contentsOfFile: gifURL.path)
  5967. // imageGif.shouldCustomLoopCount = true
  5968. // imageGif.animationRepeatCount = 4
  5969. } else if FileEncryption.shared.isSecureExists(filename: gifChat){
  5970. do {
  5971. if var data = try FileEncryption.shared.readSecure(filename: gifChat) {
  5972. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
  5973. if dataDecrypt != nil {
  5974. data = dataDecrypt!
  5975. }
  5976. if let imageData = SDAnimatedImage(data: data) {
  5977. imageGif.image = imageData
  5978. // imageGif.shouldCustomLoopCount = true
  5979. // imageGif.animationRepeatCount = 4
  5980. }
  5981. }
  5982. }
  5983. catch {
  5984. print("Error reading secure file")
  5985. }
  5986. }
  5987. }
  5988. }
  5989. }
  5990. if (dataMessages[indexPath.row]["progress"] as! Double != 100.0 && dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  5991. let container = UIView()
  5992. imageThumb.addSubview(container)
  5993. container.translatesAutoresizingMaskIntoConstraints = false
  5994. container.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
  5995. container.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
  5996. container.widthAnchor.constraint(equalToConstant: 30).isActive = true
  5997. container.heightAnchor.constraint(equalToConstant: 30).isActive = true
  5998. container.backgroundColor = .white.withAlphaComponent(0.1)
  5999. let circlePath = UIBezierPath(arcCenter: CGPoint(x: 10, y: 20), radius: 15, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
  6000. let trackShape = CAShapeLayer()
  6001. trackShape.path = circlePath.cgPath
  6002. trackShape.fillColor = UIColor.black.withAlphaComponent(0.3).cgColor
  6003. trackShape.lineWidth = 3
  6004. trackShape.strokeColor = UIColor.blueBubbleColor.withAlphaComponent(0.3).cgColor
  6005. container.backgroundColor = .clear
  6006. container.layer.addSublayer(trackShape)
  6007. let shapeLoading = CAShapeLayer()
  6008. shapeLoading.path = circlePath.cgPath
  6009. shapeLoading.fillColor = UIColor.clear.cgColor
  6010. shapeLoading.lineWidth = 3
  6011. shapeLoading.strokeEnd = 0
  6012. shapeLoading.strokeColor = UIColor.blueBubbleColor.cgColor
  6013. container.layer.addSublayer(shapeLoading)
  6014. let imageupload = UIImageView(image: UIImage(systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
  6015. imageupload.tintColor = .white
  6016. container.addSubview(imageupload)
  6017. imageupload.translatesAutoresizingMaskIntoConstraints = false
  6018. imageupload.bottomAnchor.constraint(equalTo: imageThumb.bottomAnchor, constant: -10).isActive = true
  6019. imageupload.leadingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: 10).isActive = true
  6020. imageupload.widthAnchor.constraint(equalToConstant: 20).isActive = true
  6021. imageupload.heightAnchor.constraint(equalToConstant: 20).isActive = true
  6022. }
  6023. if !copySession && !forwardSession && !deleteSession {
  6024. let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
  6025. let sfs = (dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String) ?? ""
  6026. imageThumb.isUserInteractionEnabled = true
  6027. imageThumb.addGestureRecognizer(objectTap)
  6028. objectTap.image_id = imageChat
  6029. objectTap.video_id = videoChat
  6030. objectTap.gif_id = gifChat
  6031. objectTap.specFile = sfs
  6032. objectTap.imageView = imageThumb
  6033. objectTap.indexPath = indexPath
  6034. }
  6035. }
  6036. }
  6037. if (!fileChat.isEmpty && dataMessages[indexPath.row]["lock"] as? String ?? "" != "1" && dataMessages[indexPath.row]["lock"] as? String != "2") {
  6038. topMarginText.constant = topMarginText.constant + 55
  6039. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  6040. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  6041. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  6042. let arrExtFile = (originalMessageText.components(separatedBy: "|")[0]).split(separator: ".")
  6043. let finalExtFile = arrExtFile[arrExtFile.count - 1]
  6044. if let dirPath = paths.first {
  6045. let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(fileChat)
  6046. if FileManager.default.fileExists(atPath: fileURL.path) {
  6047. if let dataFile = try? Data(contentsOf: fileURL), textChat.isEmpty {
  6048. var sizeOfFile = Int(dataFile.count / 1000000)
  6049. if (sizeOfFile < 1) {
  6050. sizeOfFile = Int(dataFile.count / 1000)
  6051. if (finalExtFile.count > 4) {
  6052. messageText.text = "\(sizeOfFile) kB \u{2022} TXT"
  6053. }else {
  6054. messageText.text = "\(sizeOfFile) kB \u{2022} \(finalExtFile.uppercased())"
  6055. }
  6056. } else {
  6057. if (finalExtFile.count > 4) {
  6058. messageText.text = "\(sizeOfFile) MB \u{2022} TXT"
  6059. }else {
  6060. messageText.text = "\(sizeOfFile) MB \u{2022} \(finalExtFile.uppercased())"
  6061. }
  6062. }
  6063. }
  6064. }
  6065. else if FileEncryption.shared.isSecureExists(filename: fileChat) {
  6066. do {
  6067. if var dataFile = try FileEncryption.shared.readSecure(filename: fileChat), textChat.isEmpty {
  6068. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: dataFile)
  6069. if dataDecrypt != nil {
  6070. dataFile = dataDecrypt!
  6071. }
  6072. var sizeOfFile = Int(dataFile.count / 1000000)
  6073. if (sizeOfFile < 1) {
  6074. sizeOfFile = Int(dataFile.count / 1000)
  6075. if (finalExtFile.count > 4) {
  6076. messageText.text = "\(sizeOfFile) kB \u{2022} TXT"
  6077. }else {
  6078. messageText.text = "\(sizeOfFile) kB \u{2022} \(finalExtFile.uppercased())"
  6079. }
  6080. } else {
  6081. if (finalExtFile.count > 4) {
  6082. messageText.text = "\(sizeOfFile) MB \u{2022} TXT"
  6083. }else {
  6084. messageText.text = "\(sizeOfFile) MB \u{2022} \(finalExtFile.uppercased())"
  6085. }
  6086. }
  6087. }
  6088. } catch {
  6089. }
  6090. }
  6091. }
  6092. containerMessage.addSubview(containerViewFile)
  6093. containerViewFile.translatesAutoresizingMaskIntoConstraints = false
  6094. let data = queryMessageReply(message_id: reffChat)
  6095. if (reffChat.isEmpty || data.count == 0) && (dataMessages[indexPath.row][TypeDataMessage.is_forwarded] == nil || dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int == 0) {
  6096. containerViewFile.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 37).isActive = true
  6097. } else {
  6098. containerViewFile.heightAnchor.constraint(greaterThanOrEqualToConstant: 50).isActive = true
  6099. }
  6100. containerViewFile.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  6101. containerViewFile.bottomAnchor.constraint(equalTo:messageText.topAnchor, constant: -5).isActive = true
  6102. containerViewFile.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  6103. // containerViewFile.heightAnchor.constraint(equalToConstant: 50).isActive = true
  6104. containerViewFile.backgroundColor = .black.withAlphaComponent(0.2)
  6105. containerViewFile.layer.cornerRadius = 5.0
  6106. containerViewFile.clipsToBounds = true
  6107. let imageFile = UIImageView(image: UIImage(systemName: "doc.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 30, weight: .bold, scale: .default)))
  6108. containerViewFile.addSubview(imageFile)
  6109. let nameFile = UILabel()
  6110. containerViewFile.addSubview(nameFile)
  6111. imageFile.translatesAutoresizingMaskIntoConstraints = false
  6112. imageFile.leadingAnchor.constraint(equalTo: containerViewFile.leadingAnchor, constant: 5).isActive = true
  6113. imageFile.trailingAnchor.constraint(equalTo: nameFile.leadingAnchor, constant: -5).isActive = true
  6114. imageFile.centerYAnchor.constraint(equalTo: containerViewFile.centerYAnchor).isActive = true
  6115. imageFile.widthAnchor.constraint(equalToConstant: 30).isActive = true
  6116. imageFile.heightAnchor.constraint(equalToConstant: 30).isActive = true
  6117. imageFile.tintColor = .docColor
  6118. nameFile.translatesAutoresizingMaskIntoConstraints = false
  6119. nameFile.centerYAnchor.constraint(equalTo: containerViewFile.centerYAnchor).isActive = true
  6120. nameFile.widthAnchor.constraint(lessThanOrEqualToConstant: 200).isActive = true
  6121. nameFile.font = UIFont.systemFont(ofSize: 12 + offset(), weight: .medium)
  6122. nameFile.textColor = .white
  6123. nameFile.text = originalMessageText.components(separatedBy: "|")[0]
  6124. if (dataMessages[indexPath.row]["progress"] as! Double != 100.0) {
  6125. let containerLoading = UIView()
  6126. containerViewFile.addSubview(containerLoading)
  6127. containerLoading.translatesAutoresizingMaskIntoConstraints = false
  6128. containerLoading.centerYAnchor.constraint(equalTo: containerViewFile.centerYAnchor).isActive = true
  6129. containerLoading.leadingAnchor.constraint(equalTo: nameFile.trailingAnchor, constant: 5).isActive = true
  6130. containerLoading.trailingAnchor.constraint(equalTo: containerViewFile.trailingAnchor, constant: -5).isActive = true
  6131. containerLoading.widthAnchor.constraint(equalToConstant: 30).isActive = true
  6132. containerLoading.heightAnchor.constraint(equalToConstant: 30).isActive = true
  6133. let circlePath = UIBezierPath(arcCenter: CGPoint(x: 15, y: 15), radius: 10, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
  6134. let trackShape = CAShapeLayer()
  6135. trackShape.path = circlePath.cgPath
  6136. trackShape.fillColor = UIColor.clear.cgColor
  6137. trackShape.lineWidth = 5
  6138. trackShape.strokeColor = UIColor.blueBubbleColor.withAlphaComponent(0.3).cgColor
  6139. containerLoading.layer.addSublayer(trackShape)
  6140. let shapeLoading = CAShapeLayer()
  6141. shapeLoading.path = circlePath.cgPath
  6142. shapeLoading.fillColor = UIColor.clear.cgColor
  6143. shapeLoading.lineWidth = 3
  6144. shapeLoading.strokeEnd = 0
  6145. shapeLoading.strokeColor = UIColor.secondaryColor.cgColor
  6146. containerLoading.layer.addSublayer(shapeLoading)
  6147. var imageupload = UIImageView(image: UIImage(systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
  6148. if dataMessages[indexPath.row]["f_pin"] as? String != idMe {
  6149. imageupload = UIImageView(image: UIImage(systemName: "arrow.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
  6150. shapeLoading.strokeColor = UIColor.blueBubbleColor.cgColor
  6151. }
  6152. imageupload.tintColor = .white
  6153. containerLoading.addSubview(imageupload)
  6154. imageupload.translatesAutoresizingMaskIntoConstraints = false
  6155. imageupload.centerYAnchor.constraint(equalTo: containerLoading.centerYAnchor).isActive = true
  6156. imageupload.centerXAnchor.constraint(equalTo: containerLoading.centerXAnchor).isActive = true
  6157. } else {
  6158. nameFile.trailingAnchor.constraint(equalTo: containerViewFile.trailingAnchor, constant: -5).isActive = true
  6159. }
  6160. if !copySession && !forwardSession && !deleteSession {
  6161. let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
  6162. let sfs = (dataMessages[indexPath.row][TypeDataMessage.spec_file] as? String) ?? ""
  6163. containerViewFile.addGestureRecognizer(objectTap)
  6164. objectTap.containerFile = containerViewFile
  6165. objectTap.labelFile = nameFile
  6166. objectTap.file_id = fileChat
  6167. objectTap.specFile = sfs
  6168. objectTap.indexPath = indexPath
  6169. }
  6170. }
  6171. let containerLinkMessage = UIView()
  6172. var isLoadingShowLink = false
  6173. if thumbChat.isEmpty && fileChat.isEmpty && !textChat.isEmpty {
  6174. var text = ""
  6175. let listTextSplitBreak = textChat.components(separatedBy: "\n")
  6176. let indexFirstLinkSplitBreak = listTextSplitBreak.firstIndex(where: { $0.contains("www.") || $0.contains("http://") || $0.contains("https://") })
  6177. if indexFirstLinkSplitBreak != nil {
  6178. let listTextSplitSpace = listTextSplitBreak[indexFirstLinkSplitBreak!].components(separatedBy: " ")
  6179. let indexFirstLinkSplitSpace = listTextSplitSpace.firstIndex(where: { ($0.starts(with: "www.") && $0.components(separatedBy: ".").count > 2) || ($0.starts(with: "http://") && $0.components(separatedBy: ".").count > 1) || ($0.starts(with: "https://") && $0.components(separatedBy: ".").count > 1) })
  6180. if indexFirstLinkSplitSpace != nil {
  6181. text = listTextSplitSpace[indexFirstLinkSplitSpace!]
  6182. }
  6183. }
  6184. if !text.isEmpty {
  6185. isLoadingShowLink = true
  6186. var dataURL = ""
  6187. func showLink() {
  6188. if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  6189. let title = data["title"] as? String
  6190. let description = data["description"] as? String
  6191. let imageUrl = data["imageUrl"] as? String
  6192. let link = data["link"] as? String
  6193. topMarginText.constant = topMarginText.constant + 85
  6194. containerMessage.addSubview(containerLinkMessage)
  6195. containerLinkMessage.translatesAutoresizingMaskIntoConstraints = false
  6196. containerLinkMessage.leadingAnchor.constraint(equalTo:containerMessage.leadingAnchor, constant: 15).isActive = true
  6197. if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" {
  6198. containerLinkMessage.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
  6199. } else {
  6200. containerLinkMessage.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
  6201. }
  6202. containerLinkMessage.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  6203. containerLinkMessage.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
  6204. containerLinkMessage.backgroundColor = .gray.withAlphaComponent(0.2)
  6205. let imagePreview = UIImageView()
  6206. if imageUrl != nil {
  6207. containerLinkMessage.addSubview(imagePreview)
  6208. imagePreview.translatesAutoresizingMaskIntoConstraints = false
  6209. imagePreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor).isActive = true
  6210. imagePreview.bottomAnchor.constraint(equalTo: containerLinkMessage.bottomAnchor).isActive = true
  6211. imagePreview.topAnchor.constraint(equalTo: containerLinkMessage.topAnchor).isActive = true
  6212. imagePreview.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
  6213. imagePreview.loadImageAsync(with: imageUrl)
  6214. imagePreview.contentMode = .scaleAspectFill
  6215. imagePreview.clipsToBounds = true
  6216. }
  6217. let titlePreview = UILabel()
  6218. containerLinkMessage.addSubview(titlePreview)
  6219. titlePreview.translatesAutoresizingMaskIntoConstraints = false
  6220. if imageUrl != nil {
  6221. titlePreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
  6222. } else {
  6223. titlePreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
  6224. }
  6225. titlePreview.topAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: 10.0).isActive = true
  6226. titlePreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -5.0).isActive = true
  6227. titlePreview.text = title
  6228. titlePreview.font = UIFont.systemFont(ofSize: 12.0 + offset(), weight: .bold)
  6229. titlePreview.textColor = self.traitCollection.userInterfaceStyle == .dark ? .white : .black
  6230. let descPreview = UILabel()
  6231. containerLinkMessage.addSubview(descPreview)
  6232. descPreview.translatesAutoresizingMaskIntoConstraints = false
  6233. if imageUrl != nil {
  6234. descPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
  6235. } else {
  6236. descPreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
  6237. }
  6238. descPreview.topAnchor.constraint(equalTo: titlePreview.bottomAnchor).isActive = true
  6239. descPreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -5.0).isActive = true
  6240. descPreview.text = description
  6241. descPreview.font = UIFont.systemFont(ofSize: 12.0 + offset())
  6242. descPreview.textColor = .gray
  6243. descPreview.numberOfLines = 1
  6244. let linkPreview = UILabel()
  6245. containerLinkMessage.addSubview(linkPreview)
  6246. linkPreview.translatesAutoresizingMaskIntoConstraints = false
  6247. if imageUrl != nil {
  6248. linkPreview.leadingAnchor.constraint(equalTo: imagePreview.trailingAnchor, constant: 5.0).isActive = true
  6249. } else {
  6250. linkPreview.leadingAnchor.constraint(equalTo: containerLinkMessage.leadingAnchor, constant: 5.0).isActive = true
  6251. }
  6252. linkPreview.topAnchor.constraint(equalTo: descPreview.bottomAnchor, constant: 8.0).isActive = true
  6253. linkPreview.trailingAnchor.constraint(equalTo: containerLinkMessage.trailingAnchor, constant: -5.0).isActive = true
  6254. linkPreview.text = link
  6255. linkPreview.font = UIFont.systemFont(ofSize: 10.0 + offset())
  6256. linkPreview.textColor = .gray
  6257. linkPreview.numberOfLines = 1
  6258. if dataMessages[indexPath.row][TypeDataMessage.is_forwarded] != nil && dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int != 0 {
  6259. showForwardedSign()
  6260. }
  6261. if !copySession && !forwardSession && !deleteSession {
  6262. let objectTap = ObjectGesture(target: self, action: #selector(tapMessageText(_:)))
  6263. objectTap.message_id = text
  6264. containerLinkMessage.addGestureRecognizer(objectTap)
  6265. }
  6266. }
  6267. }
  6268. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  6269. do {
  6270. if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select data_link from LINK_PREVIEW where link='\(text)'"), cursor.next() {
  6271. if let data = cursor.string(forColumnIndex: 0) {
  6272. dataURL = data
  6273. }
  6274. cursor.close()
  6275. }
  6276. } catch {
  6277. rollback.pointee = true
  6278. print("Access database error: \(error.localizedDescription)")
  6279. }
  6280. })
  6281. if !dataURL.isEmpty {
  6282. if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  6283. let imageUrl = data["imageUrl"] as? String
  6284. let link = data["link"] as? String ?? ""
  6285. if imageUrl == nil || (link.contains("youtube.com") && link.contains("watch?v=") && !imageUrl!.contains("img.youtube.com/vi/")) {
  6286. dataURL = ""
  6287. }
  6288. }
  6289. }
  6290. if !dataURL.isEmpty {
  6291. if let data = try! JSONSerialization.jsonObject(with: dataURL.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
  6292. let imageUrl = data["imageUrl"] as? String
  6293. let link = data["link"] as? String ?? ""
  6294. if imageUrl == nil || (link.contains("youtube.com") && link.contains("watch?v=") && !imageUrl!.contains("img.youtube.com/vi/")) {
  6295. dataURL = ""
  6296. }
  6297. }
  6298. }
  6299. if dataURL.isEmpty {
  6300. let urlConfig = URLSessionConfiguration.default
  6301. let sessionDelegate = SelfSignedURLSessionDelegate()
  6302. let session = URLSession(configuration: urlConfig, delegate: sessionDelegate, delegateQueue: nil)
  6303. let slp = SwiftLinkPreview(session: session,
  6304. workQueue: SwiftLinkPreview.defaultWorkQueue,
  6305. responseQueue: DispatchQueue.main,
  6306. cache: DisabledCache.instance)
  6307. let preview = slp.preview(text,
  6308. onSuccess: { result in
  6309. let title = result.title?.trimmingCharacters(in: .whitespacesAndNewlines)
  6310. .nilIfEmpty ?? URL(string: text)?.host ?? "Untitled"
  6311. let description: String
  6312. if text.contains("google.com") {
  6313. description = "" // special rule for google
  6314. } else {
  6315. description = result.description?.trimmingCharacters(in: .whitespacesAndNewlines)
  6316. .nilIfEmpty ?? ""
  6317. }
  6318. let imageUrl = self.youtubeThumbnail(from: text)
  6319. ?? result.image
  6320. ?? result.icon
  6321. ?? ""
  6322. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  6323. do {
  6324. var dataJson: [String: Any] = [:]
  6325. dataJson["title"] = title
  6326. dataJson["description"] = description
  6327. dataJson["imageUrl"] = imageUrl
  6328. dataJson["link"] = text
  6329. guard let json = String(data: try! JSONSerialization.data(withJSONObject: dataJson, options: []), encoding: String.Encoding.utf8) else {
  6330. return
  6331. }
  6332. _ = try Database.shared.insertRecord(fmdb: fmdb, table: "LINK_PREVIEW", cvalues: [
  6333. "id" : "\(Date().currentTimeMillis().toHex())",
  6334. "link" : text,
  6335. "data_link" : json,
  6336. "retry": 0
  6337. ], replace: true)
  6338. dataURL = json
  6339. showLink()
  6340. DispatchQueue.main.async {
  6341. tableView.reloadRows(at: [indexPath], with: .none)
  6342. }
  6343. } catch {
  6344. rollback.pointee = true
  6345. print("Access database error: \(error.localizedDescription)")
  6346. }
  6347. })
  6348. }, onError: { error in
  6349. })
  6350. } else {
  6351. showLink()
  6352. }
  6353. }
  6354. }
  6355. if (!reffChat.isEmpty) {
  6356. let data = queryMessageReply(message_id: reffChat)
  6357. if data.count != 0 {
  6358. let containerReply = UIView()
  6359. containerMessage.addSubview(containerReply)
  6360. containerReply.translatesAutoresizingMaskIntoConstraints = false
  6361. containerReply.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  6362. containerReply.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 32).isActive = true
  6363. if thumbChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6364. containerReply.bottomAnchor.constraint(equalTo: imageThumb.topAnchor, constant: -5).isActive = true
  6365. } else if fileChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6366. containerReply.bottomAnchor.constraint(equalTo: containerViewFile.topAnchor, constant: -5).isActive = true
  6367. } else if containerMessage.subviews.contains(containerLinkMessage) {
  6368. containerReply.bottomAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: -5).isActive = true
  6369. } else if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6370. containerReply.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
  6371. } else {
  6372. containerReply.bottomAnchor.constraint(equalTo: messageText.topAnchor, constant: -5).isActive = true
  6373. }
  6374. containerReply.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  6375. let minHeightConstraint = containerReply.heightAnchor.constraint(greaterThanOrEqualToConstant: 50 + (self.offset()*3))
  6376. minHeightConstraint.priority = .defaultHigh
  6377. minHeightConstraint.isActive = true
  6378. containerReply.backgroundColor = .black.withAlphaComponent(0.2)
  6379. containerReply.layer.cornerRadius = 5
  6380. containerReply.clipsToBounds = true
  6381. if (thumbChat != "" || fileChat != "") && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6382. topMarginText = messageText.topAnchor.constraint(greaterThanOrEqualTo: containerMessage.topAnchor, constant: topMarginText.constant + 50 + (self.offset()*3))
  6383. }
  6384. let leftReply = UIView()
  6385. containerReply.addSubview(leftReply)
  6386. leftReply.translatesAutoresizingMaskIntoConstraints = false
  6387. leftReply.leadingAnchor.constraint(equalTo: containerReply.leadingAnchor).isActive = true
  6388. leftReply.topAnchor.constraint(equalTo: containerReply.topAnchor).isActive = true
  6389. leftReply.bottomAnchor.constraint(equalTo: containerReply.bottomAnchor).isActive = true
  6390. leftReply.widthAnchor.constraint(equalToConstant: 3).isActive = true
  6391. leftReply.layer.cornerRadius = 5
  6392. leftReply.clipsToBounds = true
  6393. leftReply.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMinXMinYCorner]
  6394. let titleReply = UILabel()
  6395. containerReply.addSubview(titleReply)
  6396. titleReply.translatesAutoresizingMaskIntoConstraints = false
  6397. titleReply.leadingAnchor.constraint(equalTo: leftReply.leadingAnchor, constant: 10).isActive = true
  6398. titleReply.topAnchor.constraint(equalTo: containerReply.topAnchor, constant: 10).isActive = true
  6399. titleReply.trailingAnchor.constraint(lessThanOrEqualTo: containerReply.trailingAnchor, constant: -20).isActive = true
  6400. titleReply.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  6401. if (data["f_pin"] as? String == idMe) {
  6402. titleReply.text = "You".localized()
  6403. if dataMessages[indexPath.row]["f_pin"] as? String == idMe {
  6404. titleReply.textColor = .white
  6405. leftReply.backgroundColor = .white
  6406. } else {
  6407. titleReply.textColor = .mainColor
  6408. leftReply.backgroundColor = .mainColor
  6409. }
  6410. } else {
  6411. if data["f_pin"] as? String != "-999" {
  6412. let dataProfile = getDataProfile(f_pin: data["f_pin"] as? String ?? "", message_id: data["message_id"] as? String ?? "")
  6413. titleReply.text = dataProfile["name"]
  6414. } else {
  6415. titleReply.text = "Bot"
  6416. }
  6417. if dataMessages[indexPath.row]["f_pin"] as? String == idMe {
  6418. titleReply.textColor = .white
  6419. leftReply.backgroundColor = .white
  6420. } else {
  6421. titleReply.textColor = .mainColor
  6422. leftReply.backgroundColor = .mainColor
  6423. }
  6424. }
  6425. let contentReply = UILabel()
  6426. contentReply.numberOfLines = 2
  6427. containerReply.addSubview(contentReply)
  6428. contentReply.translatesAutoresizingMaskIntoConstraints = false
  6429. contentReply.leadingAnchor.constraint(equalTo: leftReply.leadingAnchor, constant: 10).isActive = true
  6430. contentReply.bottomAnchor.constraint(equalTo: containerReply.bottomAnchor, constant: -10).isActive = true
  6431. let topConstraintContent = contentReply.topAnchor.constraint(equalTo: titleReply.bottomAnchor)
  6432. topConstraintContent.priority = .defaultHigh
  6433. topConstraintContent.isActive = true
  6434. contentReply.font = UIFont.systemFont(ofSize: 10 + offset())
  6435. let message_text = data["message_text"] as? String ?? ""
  6436. let attachment_flag = data["attachment_flag"] as? String ?? ""
  6437. let thumb_chat = data["thumb_id"] as? String ?? ""
  6438. let image_chat = data["image_id"] as? String ?? ""
  6439. let video_chat = data["video_id"] as? String ?? ""
  6440. let file_chat = data["file_id"] as? String ?? ""
  6441. if (attachment_flag == "0" && thumb_chat == "") {
  6442. contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
  6443. contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  6444. } else if (attachment_flag == "1" || image_chat != "") {
  6445. if (message_text.trimmingCharacters(in: .whitespacesAndNewlines) == "") {
  6446. contentReply.text = "📷 Photo".localized()
  6447. } else {
  6448. contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  6449. }
  6450. } else if (attachment_flag == "2" || video_chat != "") {
  6451. if (message_text.trimmingCharacters(in: .whitespacesAndNewlines) == "") {
  6452. contentReply.text = "📹 Video".localized()
  6453. } else {
  6454. contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  6455. }
  6456. } else if (attachment_flag == "6" || file_chat != ""){
  6457. contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
  6458. contentReply.text = "📄 \(message_text.components(separatedBy: "|")[0])"
  6459. } else if (attachment_flag == "11") {
  6460. contentReply.text = "❤️ Sticker"
  6461. } else if attachment_flag == "27" {
  6462. contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
  6463. contentReply.text = "📄 " + "Live Streaming".localized()
  6464. } else if attachment_flag == "26" {
  6465. contentReply.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -20).isActive = true
  6466. contentReply.text = "📄 " + "Seminar".localized()
  6467. }
  6468. contentReply.textColor = .white.withAlphaComponent(0.8)
  6469. if (attachment_flag == "1" || attachment_flag == "2" || image_chat != "" || video_chat != "") {
  6470. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  6471. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  6472. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  6473. if let dirPath = paths.first {
  6474. let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb_chat)
  6475. let image : UIImage? = {
  6476. if let img = Nexilis.imageCache.object(forKey: thumb_chat as NSString) {
  6477. return img
  6478. }
  6479. else if let img = UIImage(contentsOfFile: thumbURL.path)?.resize(target: CGSize(width: 500, height: 500)) {
  6480. Nexilis.imageCache.setObject(img, forKey: thumb_chat as NSString)
  6481. return img
  6482. }
  6483. return nil
  6484. }()
  6485. // let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
  6486. let imageThumb = UIImageView(image: image)
  6487. containerReply.addSubview(imageThumb)
  6488. imageThumb.layer.cornerRadius = 2.0
  6489. imageThumb.clipsToBounds = true
  6490. imageThumb.contentMode = .scaleAspectFill
  6491. imageThumb.translatesAutoresizingMaskIntoConstraints = false
  6492. imageThumb.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -10).isActive = true
  6493. imageThumb.centerYAnchor.constraint(equalTo: containerReply.centerYAnchor).isActive = true
  6494. imageThumb.widthAnchor.constraint(equalToConstant: 30).isActive = true
  6495. imageThumb.heightAnchor.constraint(equalToConstant: 30).isActive = true
  6496. if (attachment_flag == "2") {
  6497. let imagePlay = UIImageView(image: UIImage(systemName: "play.circle.fill"))
  6498. imageThumb.addSubview(imagePlay)
  6499. imagePlay.clipsToBounds = true
  6500. imagePlay.translatesAutoresizingMaskIntoConstraints = false
  6501. imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
  6502. imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
  6503. imagePlay.widthAnchor.constraint(equalToConstant: 10).isActive = true
  6504. imagePlay.heightAnchor.constraint(equalToConstant: 10).isActive = true
  6505. imagePlay.tintColor = .white
  6506. }
  6507. titleReply.trailingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: -20).isActive = true
  6508. contentReply.trailingAnchor.constraint(equalTo: imageThumb.leadingAnchor, constant: -20).isActive = true
  6509. }
  6510. }
  6511. if (attachment_flag == "11") {
  6512. let imageSticker = UIImageView(image: UIImage(named: (message_text.components(separatedBy: "/")[1]), in: Bundle.resourceBundle(for: Nexilis.self), with: nil))
  6513. containerReply.addSubview(imageSticker)
  6514. imageSticker.layer.cornerRadius = 2.0
  6515. imageSticker.clipsToBounds = true
  6516. imageSticker.translatesAutoresizingMaskIntoConstraints = false
  6517. imageSticker.trailingAnchor.constraint(equalTo: containerReply.trailingAnchor, constant: -10).isActive = true
  6518. imageSticker.centerYAnchor.constraint(equalTo: containerReply.centerYAnchor).isActive = true
  6519. imageSticker.widthAnchor.constraint(equalToConstant: 30).isActive = true
  6520. imageSticker.heightAnchor.constraint(equalToConstant: 30).isActive = true
  6521. titleReply.trailingAnchor.constraint(equalTo: imageSticker.leadingAnchor, constant: -20).isActive = true
  6522. contentReply.trailingAnchor.constraint(equalTo: imageSticker.leadingAnchor, constant: -20).isActive = true
  6523. }
  6524. if !copySession && !forwardSession && !deleteSession {
  6525. let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
  6526. containerReply.addGestureRecognizer(objectTap)
  6527. objectTap.indexPath = indexPath
  6528. objectTap.message_id = data["message_id"] as? String ?? ""
  6529. }
  6530. }
  6531. }
  6532. if dataMessages[indexPath.row][TypeDataMessage.is_forwarded] != nil && dataMessages[indexPath.row][TypeDataMessage.is_forwarded] as! Int != 0 && !isLoadingShowLink {
  6533. showForwardedSign()
  6534. }
  6535. func showForwardedSign() {
  6536. topMarginText.constant = topMarginText.constant + 20
  6537. let containerForwarded = UIView()
  6538. containerMessage.addSubview(containerForwarded)
  6539. containerForwarded.translatesAutoresizingMaskIntoConstraints = false
  6540. containerForwarded.leadingAnchor.constraint(equalTo: containerMessage.leadingAnchor, constant: 15).isActive = true
  6541. containerForwarded.topAnchor.constraint(equalTo: containerMessage.topAnchor, constant: 32).isActive = true
  6542. containerForwarded.trailingAnchor.constraint(equalTo: containerMessage.trailingAnchor, constant: -15).isActive = true
  6543. containerForwarded.heightAnchor.constraint(equalToConstant: 20).isActive = true
  6544. if thumbChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6545. if groupImages[messageIdChat] == nil {
  6546. containerForwarded.bottomAnchor.constraint(equalTo: imageThumb.topAnchor, constant: -5).isActive = true
  6547. }
  6548. } else if fileChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6549. containerForwarded.bottomAnchor.constraint(equalTo: containerViewFile.topAnchor, constant: -5).isActive = true
  6550. } else if containerMessage.subviews.contains(containerLinkMessage) {
  6551. containerForwarded.bottomAnchor.constraint(equalTo: containerLinkMessage.topAnchor, constant: -5).isActive = true
  6552. } else if dataMessages[indexPath.row]["attachment_flag"] as? String == "11" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"] as? String ?? "" != "1") {
  6553. containerForwarded.bottomAnchor.constraint(equalTo: imageSticker.topAnchor, constant: -5).isActive = true
  6554. }
  6555. let imageForwarded = UIImageView()
  6556. containerForwarded.addSubview(imageForwarded)
  6557. imageForwarded.anchor(top: containerForwarded.topAnchor, left: containerForwarded.leftAnchor, width: 15, height: 15)
  6558. imageForwarded.image = UIImage(systemName: "arrowshape.turn.up.right.fill")
  6559. imageForwarded.tintColor = .gray
  6560. let titleForwarded = UILabel()
  6561. containerForwarded.addSubview(titleForwarded)
  6562. titleForwarded.anchor(top: containerForwarded.topAnchor, left: imageForwarded.rightAnchor, right: containerForwarded.rightAnchor, height: 15)
  6563. titleForwarded.font = .systemFont(ofSize: 15)
  6564. let textForwarded = "Forwarded".localized()
  6565. titleForwarded.attributedText = " $\(textForwarded)$".richText()
  6566. }
  6567. if messageText.isDescendant(of: containerMessage) {
  6568. var addTopMargin = true
  6569. if !reffChat.isEmpty && dataMessages[indexPath.row]["message_scope_id"] as? String ?? "" != MessageScope.FORM {
  6570. let data = queryMessageReply(message_id: reffChat)
  6571. if data.count != 0 && (topMarginText.constant == 32.0 || topMarginText.constant == 100.0) {
  6572. addTopMargin = false
  6573. }
  6574. }
  6575. if addTopMargin{
  6576. topMarginText.isActive = true
  6577. }
  6578. }
  6579. return cellMessage
  6580. }
  6581. func youtubeThumbnail(from url: String) -> String? {
  6582. guard let url = URL(string: url) else { return nil }
  6583. let host = url.host ?? ""
  6584. if host.contains("youtube.com"),
  6585. let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
  6586. let videoId = queryItems.first(where: { $0.name == "v" })?.value {
  6587. return "https://img.youtube.com/vi/\(videoId)/hqdefault.jpg"
  6588. }
  6589. if host.contains("youtu.be") {
  6590. let videoId = url.lastPathComponent
  6591. return "https://img.youtube.com/vi/\(videoId)/hqdefault.jpg"
  6592. }
  6593. return nil
  6594. }
  6595. func playPauseAudio(indexPath: IndexPath, playButton: UIButton, progressSlider: UISlider, timeLabel: UILabel) {
  6596. guard let audioPlayer = audioPlayers[indexPath] else { return }
  6597. if audioPlayer.isPlaying {
  6598. // Pause Audio
  6599. audioPlayer.pause()
  6600. playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
  6601. timers[indexPath]?.invalidate()
  6602. } else {
  6603. // Stop other players if one is already playing
  6604. if let currentPlayingIndexPath = playingIndexPath, let currentAudioPlayer = audioPlayers[currentPlayingIndexPath] {
  6605. if currentPlayingIndexPath != indexPath {
  6606. currentAudioPlayer.pause()
  6607. timers[currentPlayingIndexPath]?.invalidate()
  6608. timers[currentPlayingIndexPath] = nil
  6609. audioPlayers[currentPlayingIndexPath] = nil
  6610. tableChatView.reloadRows(at: [currentPlayingIndexPath], with: .none)
  6611. }
  6612. }
  6613. do {
  6614. try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
  6615. try AVAudioSession.sharedInstance().setActive(true)
  6616. } catch {
  6617. }
  6618. // Play new audio
  6619. audioPlayer.play()
  6620. playButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  6621. playingIndexPath = indexPath
  6622. // Start timer to update progress
  6623. timers[indexPath] = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
  6624. progressSlider.value = Float(audioPlayer.currentTime)
  6625. timeLabel.text = self.formatTime(audioPlayer.currentTime)
  6626. }
  6627. }
  6628. }
  6629. func sliderChanged(indexPath: IndexPath, progressSlider: UISlider, timeLabel: UILabel) {
  6630. guard let audioPlayer = audioPlayers[indexPath] else { return }
  6631. audioPlayer.currentTime = TimeInterval(progressSlider.value)
  6632. timeLabel.text = formatTime(audioPlayer.currentTime)
  6633. }
  6634. func formatTime(_ time: TimeInterval) -> String {
  6635. let roundedTime = time.rounded(.up)
  6636. let minutes = Int(roundedTime) / 60
  6637. let seconds = Int(roundedTime) % 60
  6638. return String(format: "%d:%02d", minutes, seconds)
  6639. }
  6640. public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
  6641. if let finishedIndexPath = audioPlayers.first(where: { $0.value == player })?.key {
  6642. DispatchQueue.main.async {
  6643. self.timers[finishedIndexPath]?.invalidate()
  6644. self.timers[finishedIndexPath] = nil
  6645. self.playingIndexPath = nil
  6646. self.audioPlayers[finishedIndexPath] = nil
  6647. self.tableChatView.reloadRows(at: [finishedIndexPath], with: .none)
  6648. }
  6649. }
  6650. }
  6651. @objc func imageGroupingTapped(_ sender: ObjectGesture) {
  6652. let listGroupingImages = ListGroupImages()
  6653. listGroupingImages.imageTapped = sender.indexImageTapped
  6654. listGroupingImages.listGroupingImages = sender.listImageFromGrouping
  6655. listGroupingImages.titleName = titleText
  6656. listGroupingImages.isInitiator = sender.isInitiator
  6657. listGroupingImages.isPersonal = false
  6658. listGroupingImages.updateEditor = { [self] updatedData, replyData, isUpdateDelete in
  6659. if replyData.count == 0 {
  6660. if updatedData.count != 0 && !isUpdateDelete {
  6661. groupImages[sender.listImageFromGrouping[0].messageId] = updatedData
  6662. } else if updatedData.count > 0 {
  6663. let deletedForEveryoneData = updatedData.filter({ $0.dataMessage["lock"] as? String == "1" })
  6664. if deletedForEveryoneData.count != 0 {
  6665. if groupImages[sender.listImageFromGrouping[0].messageId] != nil {
  6666. var dataWillEmpty = updatedData
  6667. while dataWillEmpty.count > 0 {
  6668. if let lastIdx = dataWillEmpty.lastIndex(where: { $0.dataMessage["lock"] as? String == "1" }) {
  6669. if let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == sender.listImageFromGrouping[0].messageId }) {
  6670. if dataWillEmpty[lastIdx].messageId == sender.listImageFromGrouping[0].messageId {
  6671. self.dataMessages.remove(at: idx)
  6672. self.dataMessages.insert(dataWillEmpty[lastIdx].dataMessage, at: idx)
  6673. } else {
  6674. self.dataMessages.insert(dataWillEmpty[lastIdx].dataMessage, at: idx + 1)
  6675. }
  6676. let subData = Array(updatedData[lastIdx+1..<dataWillEmpty.count])
  6677. if subData.count >= 4 {
  6678. groupImages[subData[0].messageId] = subData
  6679. self.dataMessages.insert(subData[0].dataMessage, at: lastIdx + 1)
  6680. } else {
  6681. if subData.count > 0 {
  6682. self.dataMessages.insert(contentsOf: subData.map({ $0.dataMessage }), at: idx + (dataWillEmpty[lastIdx].messageId == sender.listImageFromGrouping[0].messageId ? 1 : 2))
  6683. }
  6684. }
  6685. }
  6686. dataWillEmpty.removeSubrange(lastIdx..<dataWillEmpty.count)
  6687. } else if dataWillEmpty.count >= 4 {
  6688. groupImages[dataWillEmpty[0].messageId] = dataWillEmpty
  6689. dataWillEmpty.removeAll()
  6690. } else {
  6691. if let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == sender.listImageFromGrouping[0].messageId }) {
  6692. self.dataMessages.remove(at: idx)
  6693. self.dataMessages.insert(contentsOf: dataWillEmpty.map({ $0.dataMessage }), at: idx)
  6694. groupImages.removeValue(forKey: sender.listImageFromGrouping[0].messageId)
  6695. }
  6696. dataWillEmpty.removeAll()
  6697. }
  6698. }
  6699. } else {
  6700. }
  6701. } else {
  6702. if updatedData.count >= 4 {
  6703. if updatedData[0].messageId == sender.listImageFromGrouping[0].messageId {
  6704. groupImages[sender.listImageFromGrouping[0].messageId] = updatedData
  6705. } else {
  6706. if let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == sender.listImageFromGrouping[0].messageId }) {
  6707. self.dataMessages.remove(at: idx)
  6708. self.dataMessages.insert(updatedData[0].dataMessage, at: idx)
  6709. groupImages.removeValue(forKey: sender.listImageFromGrouping[0].messageId)
  6710. groupImages[updatedData[0].messageId] = updatedData
  6711. }
  6712. }
  6713. } else {
  6714. if let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == sender.listImageFromGrouping[0].messageId }) {
  6715. groupImages.removeValue(forKey: sender.listImageFromGrouping[0].messageId)
  6716. self.dataMessages.remove(at: idx)
  6717. let dataMessageInGrouping = updatedData.map({ $0.dataMessage })
  6718. self.dataMessages.insert(contentsOf: dataMessageInGrouping, at: idx)
  6719. }
  6720. }
  6721. }
  6722. } else {
  6723. groupImages.removeValue(forKey: sender.listImageFromGrouping[0].messageId)
  6724. if let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == sender.listImageFromGrouping[0].messageId }) {
  6725. self.dataMessages.remove(at: idx)
  6726. }
  6727. }
  6728. DispatchQueue.main.async { [self] in
  6729. tableChatView.reloadData()
  6730. }
  6731. } else if replyData.count != 0 {
  6732. handleReply(indexPath: IndexPath(row: 0, section: 0), dataMessagesImage: replyData)
  6733. }
  6734. }
  6735. self.navigationController?.pushViewController(listGroupingImages, animated: true)
  6736. }
  6737. @objc func tapAck(_ sender: ObjectGesture) {
  6738. let indexPath = sender.indexPath
  6739. let dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexPath.section]})
  6740. if dataMessages[indexPath.row]["status"] as? String ?? "" == "8" {
  6741. return
  6742. }
  6743. if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
  6744. let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
  6745. imageView.tintColor = .white
  6746. let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
  6747. banner.show()
  6748. return
  6749. }
  6750. DispatchQueue.global().async {
  6751. var opposite_pin = self.dataGroup["group_id"] as? String ?? ""
  6752. if (self.dataTopic["chat_id"] as? String ?? "" != "") {
  6753. opposite_pin = self.dataTopic["chat_id"] as? String ?? ""
  6754. }
  6755. let result = Nexilis.write(message: CoreMessage_TMessageBank.getAckLocationMessage(f_pin: dataMessages[indexPath.row]["f_pin"] as? String ?? "", message_id: dataMessages[indexPath.row]["message_id"] as? String ?? "", l_pin: opposite_pin, server_date: "\(Date().currentTimeMillis())", message_scope_id: dataMessages[indexPath.row]["message_scope_id"] as? String ?? "", longitude: self.longitude, latitude: self.latitude, description: ""))
  6756. if result != nil {
  6757. Database.shared.database?.inTransaction({ (fmdb, rollback) in
  6758. do {
  6759. _ = Database.shared.updateRecord(fmdb: fmdb, table: "MESSAGE", cvalues: [
  6760. "status" : "8"
  6761. ], _where: "message_id = '\(dataMessages[indexPath.row]["message_id"] as? String ?? "")'")
  6762. } catch {
  6763. rollback.pointee = true
  6764. print("Access database error: \(error.localizedDescription)")
  6765. }
  6766. })
  6767. DispatchQueue.main.async {
  6768. if let index = self.dataMessages.firstIndex(where: {$0["message_id"] as? String == dataMessages[indexPath.row]["message_id"] as? String}) {
  6769. self.dataMessages[index]["status"] = "8"
  6770. let section = self.dataDates.firstIndex(of: self.dataMessages[index]["chat_date"] as? String ?? "")
  6771. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String ?? "" == self.dataMessages[index]["message_id"] as? String ?? ""})
  6772. if row != nil && section != nil {
  6773. self.tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .none)
  6774. }
  6775. self.view.makeToast("Confirmation Success.".localized(), duration: 3)
  6776. }
  6777. }
  6778. }
  6779. }
  6780. }
  6781. @objc func contentMessageTapped(_ sender: ObjectGesture) {
  6782. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  6783. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  6784. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  6785. func showMedia(data: Data? = nil, url: URL? = nil, type: Int = 0) {
  6786. let image = UIImage(data: data ?? Data())
  6787. let imageViewer = MediaViewerViewController()
  6788. if type == 0 {
  6789. imageViewer.media = .image(image ?? UIImage())
  6790. } else if type == 1 {
  6791. imageViewer.media = .video(url ?? URL(string: "")!)
  6792. } else if type == 2 {
  6793. imageViewer.media = .gif(UIImage.gifImageWithData(data ?? Data()) ?? UIImage())
  6794. }
  6795. let navigationController = UINavigationController(rootViewController: imageViewer)
  6796. navigationController.defaultStyle()
  6797. navigationController.view.backgroundColor = .clear
  6798. navigationController.modalPresentationCapturesStatusBarAppearance = true
  6799. navigationController.modalPresentationStyle = .overFullScreen
  6800. let backAction = UIAction { _ in
  6801. navigationController.dismiss(animated: true)
  6802. }
  6803. let backButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "chevron.backward"), primaryAction: backAction, menu: nil)
  6804. imageViewer.navigationItem.leftBarButtonItem = backButton
  6805. if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
  6806. let shareAction = UIAction { _ in
  6807. var activityViewController = UIActivityViewController(activityItems: [""], applicationActivities: nil)
  6808. if type == 1 {
  6809. activityViewController = UIActivityViewController(activityItems: [url ?? URL(string: "")!], applicationActivities: nil)
  6810. } else {
  6811. let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("ImageSharedNexilis-\(Date().currentTimeMillis())" + ".jpeg")
  6812. try? data!.write(to: tempURL)
  6813. activityViewController = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
  6814. }
  6815. activityViewController.popoverPresentationController?.sourceView = imageViewer.view
  6816. imageViewer.present(activityViewController, animated: true, completion: nil)
  6817. }
  6818. let shareButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "square.and.arrow.up"), primaryAction: shareAction, menu: nil)
  6819. imageViewer.navigationItem.rightBarButtonItem = shareButton
  6820. }
  6821. let name = (dataGroup["f_name"] as? String ?? "") + " (\(dataTopic["title"] as? String ?? ""))"
  6822. imageViewer.title = name
  6823. let transitionDelegate = ZoomTransitioningDelegate()
  6824. transitionDelegate.originImageView = sender.imageView
  6825. navigationController.transitioningDelegate = transitionDelegate
  6826. self.transitioningDelegateRef = transitionDelegate
  6827. present(navigationController, animated: true) {
  6828. imageViewer.animateBackgroundIn()
  6829. }
  6830. }
  6831. if (sender.image_id != "") {
  6832. if let dirPath = paths.first {
  6833. let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.image_id)
  6834. if FileManager.default.fileExists(atPath: imageURL.path) {
  6835. do {
  6836. showMedia(data: try Data(contentsOf: imageURL))
  6837. } catch {
  6838. }
  6839. } else if FileEncryption.shared.isSecureExists(filename: sender.image_id) {
  6840. do {
  6841. if var data = try FileEncryption.shared.readSecure(filename: sender.image_id) {
  6842. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: data)
  6843. if dataDecrypt != nil {
  6844. data = dataDecrypt!
  6845. }
  6846. showMedia(data: data)
  6847. }
  6848. }
  6849. catch {
  6850. print("Error reading secure file")
  6851. }
  6852. } else {
  6853. for view in sender.imageView.subviews {
  6854. if view is UIImageView {
  6855. view.removeFromSuperview()
  6856. }
  6857. }
  6858. let activityIndicator = UIActivityIndicatorView(style: .large)
  6859. activityIndicator.color = .mainColor
  6860. activityIndicator.hidesWhenStopped = true
  6861. activityIndicator.center = CGPoint(x:sender.imageView.frame.width/2,
  6862. y: sender.imageView.frame.height/2)
  6863. activityIndicator.startAnimating()
  6864. sender.imageView.addSubview(activityIndicator)
  6865. Download().startHTTP(forKey: sender.image_id) { (name, progress) in
  6866. guard progress == 100 else {
  6867. return
  6868. }
  6869. DispatchQueue.main.async {
  6870. activityIndicator.stopAnimating()
  6871. self.tableChatView.reloadRows(at: [sender.indexPath], with: .none)
  6872. }
  6873. }
  6874. }
  6875. }
  6876. } else if (sender.gif_id != "") {
  6877. if let dirPath = paths.first {
  6878. let gifURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.gif_id)
  6879. if FileManager.default.fileExists(atPath: gifURL.path) {
  6880. do {
  6881. let data = try Data(contentsOf: gifURL)
  6882. showMedia(data: data, type: 2)
  6883. } catch {
  6884. }
  6885. } else if FileEncryption.shared.isSecureExists(filename: sender.gif_id) {
  6886. do {
  6887. if var secureData = try FileEncryption.shared.readSecure(filename: sender.gif_id) {
  6888. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: secureData)
  6889. if dataDecrypt != nil {
  6890. secureData = dataDecrypt!
  6891. }
  6892. showMedia(data: secureData, type: 2)
  6893. }
  6894. } catch {
  6895. }
  6896. }
  6897. }
  6898. } else if (sender.video_id != "") {
  6899. if let dirPath = paths.first {
  6900. let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.video_id)
  6901. if FileManager.default.fileExists(atPath: videoURL.path) {
  6902. showMedia(url: videoURL, type: 1)
  6903. } else if FileEncryption.shared.isSecureExists(filename: sender.video_id) {
  6904. do {
  6905. if var secureData = try FileEncryption.shared.readSecure(filename: sender.video_id) {
  6906. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: secureData)
  6907. if dataDecrypt != nil {
  6908. secureData = dataDecrypt!
  6909. }
  6910. let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
  6911. let tempPath = cachesDirectory.appendingPathComponent(sender.video_id)
  6912. try secureData.write(to: tempPath)
  6913. showMedia(url: tempPath, type: 1)
  6914. }
  6915. } catch {
  6916. }
  6917. } else {
  6918. if downloadList[sender.video_id] != nil && downloadList[sender.video_id] == sender.indexPath {
  6919. return
  6920. }
  6921. downloadList[sender.video_id] = sender.indexPath
  6922. for view in sender.imageView.subviews {
  6923. if view is UIImageView {
  6924. view.removeFromSuperview()
  6925. }
  6926. }
  6927. let container = UIView()
  6928. sender.imageView.addSubview(container)
  6929. container.translatesAutoresizingMaskIntoConstraints = false
  6930. container.centerXAnchor.constraint(equalTo: sender.imageView.centerXAnchor).isActive = true
  6931. container.centerYAnchor.constraint(equalTo: sender.imageView.centerYAnchor).isActive = true
  6932. container.widthAnchor.constraint(equalToConstant: 50).isActive = true
  6933. container.heightAnchor.constraint(equalToConstant: 50).isActive = true
  6934. let circlePath = UIBezierPath(arcCenter: CGPoint(x: 25, y: 25), radius: 20, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
  6935. let trackShape = CAShapeLayer()
  6936. trackShape.path = circlePath.cgPath
  6937. trackShape.fillColor = UIColor.clear.cgColor
  6938. trackShape.lineWidth = 10
  6939. trackShape.strokeColor = UIColor.mentionColor.withAlphaComponent(0.3).cgColor
  6940. container.backgroundColor = .clear
  6941. container.layer.addSublayer(trackShape)
  6942. let shapeLoading = CAShapeLayer()
  6943. shapeLoading.path = circlePath.cgPath
  6944. shapeLoading.fillColor = UIColor.clear.cgColor
  6945. shapeLoading.lineWidth = 10
  6946. shapeLoading.strokeEnd = 0
  6947. shapeLoading.strokeColor = UIColor.mentionColor.cgColor
  6948. container.layer.addSublayer(shapeLoading)
  6949. let imageDownload = UIImageView(image: UIImage(systemName: "arrow.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
  6950. imageDownload.tintColor = .white
  6951. container.addSubview(imageDownload)
  6952. imageDownload.translatesAutoresizingMaskIntoConstraints = false
  6953. imageDownload.centerXAnchor.constraint(equalTo: sender.imageView.centerXAnchor).isActive = true
  6954. imageDownload.centerYAnchor.constraint(equalTo: sender.imageView.centerYAnchor).isActive = true
  6955. imageDownload.widthAnchor.constraint(equalToConstant: 30).isActive = true
  6956. imageDownload.heightAnchor.constraint(equalToConstant: 30).isActive = true
  6957. Download().startHTTP(forKey: sender.video_id) { (name, progress) in
  6958. DispatchQueue.main.async {
  6959. guard progress == 100 else {
  6960. shapeLoading.strokeEnd = CGFloat(progress / 100)
  6961. return
  6962. }
  6963. let idx = self.dataMessages.firstIndex(where: { $0["video_id"] as? String ?? "" == sender.video_id})
  6964. if idx != nil {
  6965. self.dataMessages[idx!]["progress"] = progress
  6966. self.tableChatView.reloadRows(at: [sender.indexPath], with: .none)
  6967. }
  6968. }
  6969. }
  6970. }
  6971. }
  6972. } else if (sender.file_id != "") {
  6973. func showFile(urlFile: URL) {
  6974. let previewController = QLPreviewController()
  6975. previewController.dataSource = self
  6976. let vcHandleFile = UIViewController()
  6977. let nc = UINavigationController(rootViewController: vcHandleFile)
  6978. let attributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
  6979. let navBarAppearance = UINavigationBarAppearance()
  6980. nc.defaultStyle()
  6981. navBarAppearance.configureWithOpaqueBackground()
  6982. navBarAppearance.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .blackDarkMode : UIColor.mainColor
  6983. navBarAppearance.titleTextAttributes = attributes
  6984. nc.navigationBar.standardAppearance = navBarAppearance
  6985. nc.navigationBar.scrollEdgeAppearance = navBarAppearance
  6986. let backAction = UIAction { _ in
  6987. nc.dismiss(animated: true)
  6988. }
  6989. let backButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "chevron.backward"), primaryAction: backAction, menu: nil)
  6990. vcHandleFile.navigationItem.leftBarButtonItem = backButton
  6991. if Nexilis.checkingAccess(key: "secure_folder_share") || sender.specFile.contains("download") || sender.specFile.contains("share") {
  6992. let shareAction = UIAction { _ in
  6993. let fileManager = FileManager.default
  6994. let tempURL = fileManager.temporaryDirectory.appendingPathComponent(urlFile.lastPathComponent)
  6995. do {
  6996. if !fileManager.fileExists(atPath: tempURL.path) {
  6997. try fileManager.copyItem(at: urlFile, to: tempURL)
  6998. }
  6999. let activityViewController = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
  7000. activityViewController.popoverPresentationController?.sourceView = vcHandleFile.view
  7001. vcHandleFile.present(activityViewController, animated: true, completion: nil)
  7002. } catch {
  7003. }
  7004. }
  7005. let shareButton = UIBarButtonItem(title: nil, image: UIImage(systemName: "square.and.arrow.up"), primaryAction: shareAction, menu: nil)
  7006. vcHandleFile.navigationItem.rightBarButtonItem = shareButton
  7007. }
  7008. if let viewVc = vcHandleFile.view {
  7009. vcHandleFile.title = sender.labelFile.text
  7010. vcHandleFile.addChild(previewController)
  7011. previewController.dataSource = self
  7012. previewController.view.frame = CGRect(x: 0, y: 0, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height)
  7013. previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  7014. viewVc.addSubview(previewController.view)
  7015. previewController.didMove(toParent: vcHandleFile)
  7016. self.present(nc, animated: true)
  7017. }
  7018. }
  7019. if let dirPath = paths.first {
  7020. let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.file_id)
  7021. if FileManager.default.fileExists(atPath: fileURL.path) {
  7022. self.previewItem = fileURL as NSURL
  7023. showFile(urlFile: fileURL)
  7024. } else if FileEncryption.shared.isSecureExists(filename: sender.file_id) {
  7025. do {
  7026. if var docData = try FileEncryption.shared.readSecure(filename: sender.file_id) {
  7027. let dataDecrypt = FileEncryption.shared.decryptFileFromServer(data: docData)
  7028. if dataDecrypt != nil {
  7029. docData = dataDecrypt!
  7030. }
  7031. let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
  7032. let tempPath = cachesDirectory.appendingPathComponent(sender.file_id)
  7033. try docData.write(to: tempPath)
  7034. self.previewItem = tempPath as NSURL
  7035. showFile(urlFile: tempPath)
  7036. }
  7037. }
  7038. catch {
  7039. }
  7040. } else {
  7041. if downloadList[sender.file_id] != nil && downloadList[sender.file_id] == sender.indexPath {
  7042. return
  7043. }
  7044. downloadList[sender.file_id] = sender.indexPath
  7045. for view in sender.containerFile.subviews {
  7046. if !(view is UIImageView) && !(view is UILabel) {
  7047. view.removeFromSuperview()
  7048. }
  7049. }
  7050. let containerLoading = UIView()
  7051. sender.containerFile.addSubview(containerLoading)
  7052. containerLoading.translatesAutoresizingMaskIntoConstraints = false
  7053. containerLoading.centerYAnchor.constraint(equalTo: sender.containerFile.centerYAnchor).isActive = true
  7054. containerLoading.leadingAnchor.constraint(equalTo: sender.labelFile.trailingAnchor, constant: 5).isActive = true
  7055. containerLoading.trailingAnchor.constraint(equalTo: sender.containerFile.trailingAnchor, constant: -5).isActive = true
  7056. containerLoading.widthAnchor.constraint(equalToConstant: 30).isActive = true
  7057. containerLoading.heightAnchor.constraint(equalToConstant: 30).isActive = true
  7058. let circlePath = UIBezierPath(arcCenter: CGPoint(x: 15, y: 15), radius: 10, startAngle: -(.pi / 2), endAngle: .pi * 2, clockwise: true)
  7059. let trackShape = CAShapeLayer()
  7060. trackShape.path = circlePath.cgPath
  7061. trackShape.fillColor = UIColor.clear.cgColor
  7062. trackShape.lineWidth = 5
  7063. trackShape.strokeColor = UIColor.mentionColor.withAlphaComponent(0.3).cgColor
  7064. containerLoading.layer.addSublayer(trackShape)
  7065. let shapeLoading = CAShapeLayer()
  7066. shapeLoading.path = circlePath.cgPath
  7067. shapeLoading.fillColor = UIColor.clear.cgColor
  7068. shapeLoading.lineWidth = 3
  7069. shapeLoading.strokeEnd = 0
  7070. shapeLoading.strokeColor = UIColor.mentionColor.cgColor
  7071. containerLoading.layer.addSublayer(shapeLoading)
  7072. let imageupload = UIImageView(image: UIImage(systemName: "arrow.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 10, weight: .bold, scale: .default)))
  7073. imageupload.tintColor = .white
  7074. containerLoading.addSubview(imageupload)
  7075. imageupload.translatesAutoresizingMaskIntoConstraints = false
  7076. imageupload.centerYAnchor.constraint(equalTo: containerLoading.centerYAnchor).isActive = true
  7077. imageupload.centerXAnchor.constraint(equalTo: containerLoading.centerXAnchor).isActive = true
  7078. Download().startHTTP(forKey: sender.file_id) { (name, progress) in
  7079. DispatchQueue.main.async {
  7080. guard progress == 100 else {
  7081. shapeLoading.strokeEnd = CGFloat(progress / 100)
  7082. return
  7083. }
  7084. let idx = self.dataMessages.firstIndex(where: { $0["file_id"] as? String ?? "" == sender.file_id})
  7085. if idx != nil {
  7086. self.dataMessages[idx!]["progress"] = progress
  7087. self.tableChatView.reloadRows(at: [sender.indexPath], with: .none)
  7088. }
  7089. }
  7090. }
  7091. }
  7092. }
  7093. } else {
  7094. DispatchQueue.main.async {
  7095. let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String ?? "" == sender.message_id})
  7096. if idx == nil {
  7097. return
  7098. }
  7099. let section = self.dataDates.firstIndex(of: self.dataMessages[idx!]["chat_date"] as? String ?? "")
  7100. if section == nil {
  7101. return
  7102. }
  7103. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String ?? "" == self.dataMessages[idx!]["message_id"] as? String ?? ""})
  7104. if row == nil {
  7105. return
  7106. }
  7107. let indexPath = IndexPath(row: row!, section: section!)
  7108. self.tableChatView.scrollToRow(at: indexPath, at: .middle, animated: true)
  7109. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
  7110. if let cell = self.tableChatView.cellForRow(at: indexPath) {
  7111. let containerMessage = cell.contentView.subviews[1]
  7112. let idMe = User.getMyPin() as String?
  7113. if (self.dataMessages[idx!]["f_pin"] as? String == idMe) {
  7114. containerMessage.backgroundColor = .blueBubbleColor.withAlphaComponent(0.3)
  7115. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  7116. if (self.dataMessages[idx!]["attachment_flag"] as? String == "11") {
  7117. containerMessage.backgroundColor = .clear
  7118. } else {
  7119. containerMessage.backgroundColor = .blueBubbleColor
  7120. }
  7121. }
  7122. } else {
  7123. containerMessage.backgroundColor = .whiteBubbleColor.withAlphaComponent(0.3)
  7124. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  7125. if (self.dataMessages[idx!]["attachment_flag"] as? String == "11") {
  7126. containerMessage.backgroundColor = .clear
  7127. } else {
  7128. containerMessage.backgroundColor = .whiteBubbleColor
  7129. }
  7130. }
  7131. }
  7132. }
  7133. }
  7134. }
  7135. }
  7136. }
  7137. func highlightedText(for text: String, in range: Range<String.Index>, textView: UITextView) -> NSAttributedString {
  7138. let mutableAttributedString = textView.attributedText!.mutableCopy() as! NSMutableAttributedString
  7139. mutableAttributedString.addAttribute(.backgroundColor, value: UIColor.lightGray.withAlphaComponent(0.5), range: NSRange(range, in: text))
  7140. return mutableAttributedString
  7141. }
  7142. func removeHighlightedText(for text: String, in range: Range<String.Index>, textView: UITextView) -> NSAttributedString {
  7143. let mutableAttributedString = textView.attributedText!.mutableCopy() as! NSMutableAttributedString
  7144. mutableAttributedString.removeAttribute(.backgroundColor, range: NSRange(range, in: text))
  7145. return mutableAttributedString
  7146. }
  7147. @objc func tapMessageText(_ sender: ObjectGesture) {
  7148. var stringURl = sender.message_id
  7149. if stringURl.lowercased().starts(with: "www.") {
  7150. stringURl = "https://" + stringURl.replacingOccurrences(of: "www.", with: "")
  7151. }
  7152. if Nexilis.checkingAccess(key: "secure_browser") {
  7153. APIS.openUrl(url: stringURl)
  7154. } else {
  7155. guard let url = URL(string: stringURl) else { return }
  7156. UIApplication.shared.open(url)
  7157. }
  7158. }
  7159. // public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  7160. // if copySession || forwardSession || deleteSession {
  7161. // return nil
  7162. // }
  7163. // let idMe = User.getMyPin() as String?
  7164. // if (dataMessages[indexPath.row]["f_pin"] as? String != idMe) {
  7165. // return nil
  7166. // }
  7167. // let messageInfoVC = MessageInfo()
  7168. // self.navigationController?.show(messageInfoVC, sender: nil)
  7169. // return UISwipeActionsConfiguration()
  7170. // }
  7171. //
  7172. // public func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  7173. // if copySession || forwardSession || deleteSession {
  7174. // return nil
  7175. // }
  7176. // let action = UIContextualAction(style: .normal,
  7177. // title: "") { [weak self] (action, view, completionHandler) in
  7178. // self?.handleReply(indexPath: indexPath)
  7179. // completionHandler(true)
  7180. // }
  7181. // action.backgroundColor = .white
  7182. // action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?.withTintColor(.black, renderingMode: .alwaysOriginal)
  7183. // return UISwipeActionsConfiguration(actions: [action])
  7184. // }
  7185. private func pinAllMessages(dataMessages: [[String: Any?]], isPinned: Int = -1) {
  7186. var dataMessages = dataMessages
  7187. dataMessages.sort {
  7188. let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
  7189. let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
  7190. return firstPinned < secondPinned
  7191. }
  7192. if dataMessages.count != 0 {
  7193. if !self.containerPin.isDescendant(of: self.view) && dataMessages.count != 0 {
  7194. self.tableChatView.contentInset.top = 50
  7195. self.view.addSubview(self.containerPin)
  7196. self.containerPin.isUserInteractionEnabled = true
  7197. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(viewPinTapped))
  7198. self.containerPin.addGestureRecognizer(tapGesture)
  7199. self.containerPin.anchor(top: self.view.safeAreaLayoutGuide.topAnchor, left: self.view.leftAnchor, right: self.view.rightAnchor, height: 50)
  7200. self.containerPin.backgroundColor = .mainColor
  7201. if dataMessages.count > 1 {
  7202. self.containerPin.addSubview(self.signSelectedPin)
  7203. self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
  7204. self.signSelectedPin.layer.cornerRadius = 1
  7205. self.signSelectedPin.clipsToBounds = true
  7206. self.signSelectedPin.alignment = .fill
  7207. self.signSelectedPin.axis = .vertical
  7208. self.signSelectedPin.distribution = .fill
  7209. self.signSelectedPin.spacing = dataMessages.count == 3 ? 1.5 : 2
  7210. let heightSign: CGFloat = CGFloat((30 / dataMessages.count) - 1)
  7211. let widthSign: CGFloat = 2
  7212. for i in 0..<dataMessages.count {
  7213. let viewSign = UIView()
  7214. viewSign.backgroundColor = (i == (dataMessages.count - 1)) ? .white : .gray
  7215. viewSign.anchor(width: widthSign, height: heightSign)
  7216. viewSign.layer.cornerRadius = 1
  7217. viewSign.clipsToBounds = true
  7218. self.signSelectedPin.addArrangedSubview(viewSign)
  7219. }
  7220. self.nextPinShowed = dataMessages.count - 1
  7221. }
  7222. let contIconPin = UIImageView()
  7223. self.containerPin.addSubview(contIconPin)
  7224. contIconPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 15, centerY: self.containerPin.centerYAnchor, width: 30, height: 30)
  7225. contIconPin.layer.cornerRadius = 8
  7226. contIconPin.clipsToBounds = true
  7227. contIconPin.backgroundColor = .gray
  7228. contIconPin.image = UIImage(systemName: "pin.fill")?.imageWithInsets(insets: UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))?.withTintColor(.waGrayLight)
  7229. self.containerPin.addSubview(textPin)
  7230. self.textPin.anchor(left: contIconPin.rightAnchor, right: self.containerPin.rightAnchor, paddingLeft: 10, paddingRight: 10, centerY: self.containerPin.centerYAnchor)
  7231. self.textPin.attributedText = (dataMessages[dataMessages.count - 1][TypeDataMessage.message_text] as? String ?? "").richText(fontSize: 14, group_id: self.dataGroup["group_id"] as? String ?? "")
  7232. self.textPin.numberOfLines = 1
  7233. self.textPin.textColor = .white
  7234. } else {
  7235. self.signSelectedPin.subviews.forEach({ $0.removeFromSuperview() })
  7236. self.signSelectedPin.removeFromSuperview()
  7237. var same = false
  7238. if dataMessages.count > 1 {
  7239. self.containerPin.addSubview(self.signSelectedPin)
  7240. self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
  7241. self.signSelectedPin.layer.cornerRadius = 1
  7242. self.signSelectedPin.clipsToBounds = true
  7243. self.signSelectedPin.alignment = .fill
  7244. self.signSelectedPin.axis = .vertical
  7245. self.signSelectedPin.distribution = .fill
  7246. self.signSelectedPin.spacing = dataMessages.count == 3 ? 1.5 : 2
  7247. let heightSign: CGFloat = CGFloat((30 / dataMessages.count) - 1)
  7248. let widthSign: CGFloat = 2
  7249. for i in 0..<dataMessages.count {
  7250. let viewSign = UIView()
  7251. viewSign.backgroundColor = (i == (dataMessages.count - 1)) ? .white : .gray
  7252. viewSign.anchor(width: widthSign, height: heightSign)
  7253. viewSign.layer.cornerRadius = 1
  7254. viewSign.clipsToBounds = true
  7255. self.signSelectedPin.addArrangedSubview(viewSign)
  7256. }
  7257. if isPinned == -1 {
  7258. self.nextPinShowed = dataMessages.count - 1
  7259. } else if self.nextPinShowed != 0 {
  7260. if (self.nextPinShowed > isPinned) {
  7261. self.nextPinShowed-=1
  7262. same = true
  7263. } else if self.nextPinShowed == isPinned && dataMessages.count == 3 {
  7264. self.nextPinShowed-=2
  7265. }
  7266. } else if self.nextPinShowed != isPinned {
  7267. same = true
  7268. }
  7269. } else if self.nextPinShowed != isPinned {
  7270. same = true
  7271. }
  7272. if !same{
  7273. animateLabelTextChange(label: self.textPin, newText: dataMessages[dataMessages.count - 1][TypeDataMessage.message_text] as? String ?? "")
  7274. }
  7275. }
  7276. } else if self.containerPin.isDescendant(of: self.view) {
  7277. self.containerPin.subviews.forEach({ $0.removeFromSuperview() })
  7278. self.containerPin.removeFromSuperview()
  7279. self.tableChatView.contentInset.top = 0
  7280. }
  7281. }
  7282. @objc func viewPinTapped() {
  7283. var dataMessagesPin = self.dataMessages.filter({ $0[TypeDataMessage.is_pinned] as? String ?? "0" != "0"})
  7284. dataMessagesPin.sort {
  7285. let firstPinned = Int64($0[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
  7286. let secondPinned = Int64($1[TypeDataMessage.is_pinned] as? String ?? "0") ?? 0
  7287. return firstPinned < secondPinned
  7288. }
  7289. let obj = ObjectGesture()
  7290. obj.message_id = dataMessagesPin[nextPinShowed][TypeDataMessage.message_id] as? String ?? ""
  7291. contentMessageTapped(obj)
  7292. if dataMessagesPin.count > 0 {
  7293. if nextPinShowed < dataMessagesPin.count - 1 {
  7294. nextPinShowed+=1
  7295. } else {
  7296. nextPinShowed = 0
  7297. }
  7298. DispatchQueue.main.async {
  7299. self.signSelectedPin.subviews.forEach({ $0.removeFromSuperview() })
  7300. self.signSelectedPin.removeFromSuperview()
  7301. self.containerPin.addSubview(self.signSelectedPin)
  7302. self.signSelectedPin.anchor(left: self.containerPin.leftAnchor, paddingLeft: 8, centerY: self.containerPin.centerYAnchor, width: 2, height: 30)
  7303. self.signSelectedPin.layer.cornerRadius = 1
  7304. self.signSelectedPin.clipsToBounds = true
  7305. self.signSelectedPin.alignment = .fill
  7306. self.signSelectedPin.axis = .vertical
  7307. self.signSelectedPin.distribution = .fill
  7308. self.signSelectedPin.spacing = dataMessagesPin.count == 3 ? 1.5 : 2
  7309. let heightSign: CGFloat = CGFloat((30 / dataMessagesPin.count) - 1)
  7310. let widthSign: CGFloat = 2
  7311. for i in 0..<dataMessagesPin.count {
  7312. let viewSign = UIView()
  7313. viewSign.backgroundColor = (i == self.nextPinShowed) ? .white : .gray
  7314. viewSign.anchor(width: widthSign, height: heightSign)
  7315. viewSign.layer.cornerRadius = 1
  7316. viewSign.clipsToBounds = true
  7317. self.signSelectedPin.addArrangedSubview(viewSign)
  7318. }
  7319. self.animateLabelTextChange(label: self.textPin, newText: dataMessagesPin[self.nextPinShowed][TypeDataMessage.message_text] as? String ?? "")
  7320. }
  7321. }
  7322. }
  7323. func animateLabelTextChange(label: UILabel, newText: String) {
  7324. let animationDuration = 0.1
  7325. UIView.animate(withDuration: animationDuration, animations: {
  7326. label.transform = CGAffineTransform(translationX: 0, y: -10)
  7327. label.alpha = 0
  7328. }) { _ in
  7329. // Change text after fade out
  7330. label.attributedText = newText.richText(fontSize: 14, group_id: self.dataGroup["group_id"] as? String ?? "")
  7331. label.transform = CGAffineTransform(translationX: 0, y: 10)
  7332. // Animate back to original position and fade in
  7333. UIView.animate(withDuration: animationDuration) {
  7334. label.transform = .identity
  7335. label.alpha = 1
  7336. }
  7337. }
  7338. }
  7339. private func handleReply(indexPath: IndexPath, dataMessagesImage: [String: Any?] = [:], reffId: String = "") {
  7340. var dataMessages = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == dataDates[indexPath.section]})
  7341. if reffId.isEmpty {
  7342. self.deleteReplyView()
  7343. if dataMessagesImage.count != 0 {
  7344. dataMessages = [dataMessagesImage]
  7345. } else {
  7346. self.textFieldSend.becomeFirstResponder()
  7347. }
  7348. self.reffId = dataMessages[indexPath.row]["message_id"] as? String
  7349. } else {
  7350. dataMessages = self.dataMessages.filter({ $0["message_id"] as? String ?? "" == reffId })
  7351. self.reffId = reffId
  7352. }
  7353. if dataMessages.count == 0 {
  7354. self.deleteReplyView()
  7355. return
  7356. }
  7357. UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
  7358. self.constraintTopTextField.constant = self.constraintTopTextField.constant + 50 + (self.offset()*3)
  7359. if self.contraintBottomMention.constant > 0 {
  7360. self.contraintBottomMention.constant = self.contraintBottomMention.constant + self.heightTextFieldSend.constant
  7361. }
  7362. }, completion: nil)
  7363. if (self.currentIndexpath != nil) {
  7364. DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
  7365. self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
  7366. }
  7367. } else {
  7368. self.tableChatView.scrollToBottom()
  7369. }
  7370. self.viewTextfield.addSubview(self.containerPreviewReply)
  7371. self.containerPreviewReply.translatesAutoresizingMaskIntoConstraints = false
  7372. self.containerPreviewReply.leadingAnchor.constraint(equalTo: self.viewTextfield.leadingAnchor).isActive = true
  7373. self.containerPreviewReply.topAnchor.constraint(equalTo: self.viewTextfield.topAnchor).isActive = true
  7374. if !self.containerLink.isDescendant(of: self.viewTextfield) {
  7375. self.bottomAnchorPreviewReply = self.containerPreviewReply.bottomAnchor.constraint(equalTo: self.textFieldSend.topAnchor)
  7376. } else {
  7377. self.bottomAnchorPreviewReply = self.containerPreviewReply.bottomAnchor.constraint(equalTo: self.containerLink.topAnchor)
  7378. }
  7379. self.bottomAnchorPreviewReply.isActive = true
  7380. self.containerPreviewReply.trailingAnchor.constraint(equalTo: self.viewTextfield.trailingAnchor).isActive = true
  7381. self.containerPreviewReply.backgroundColor = .secondaryColor
  7382. let leftReply = UIView()
  7383. self.containerPreviewReply.addSubview(leftReply)
  7384. leftReply.translatesAutoresizingMaskIntoConstraints = false
  7385. leftReply.leadingAnchor.constraint(equalTo: self.viewTextfield.leadingAnchor).isActive = true
  7386. leftReply.topAnchor.constraint(equalTo: self.containerPreviewReply.topAnchor).isActive = true
  7387. leftReply.bottomAnchor.constraint(equalTo: self.containerPreviewReply.bottomAnchor).isActive = true
  7388. leftReply.widthAnchor.constraint(equalToConstant: 3).isActive = true
  7389. leftReply.backgroundColor = .orangeColor
  7390. let titleReply = UILabel()
  7391. self.containerPreviewReply.addSubview(titleReply)
  7392. titleReply.translatesAutoresizingMaskIntoConstraints = false
  7393. titleReply.leadingAnchor.constraint(equalTo: leftReply.leadingAnchor, constant: 10).isActive = true
  7394. titleReply.topAnchor.constraint(equalTo: self.containerPreviewReply.topAnchor, constant: 10).isActive = true
  7395. titleReply.font = UIFont.systemFont(ofSize: 12 + offset()).bold
  7396. let idMe = User.getMyPin() as String?
  7397. if (dataMessages[indexPath.row]["f_pin"] as? String == idMe) {
  7398. titleReply.text = "You".localized()
  7399. } else {
  7400. if dataMessages[indexPath.row]["f_pin"] as? String != "-999" {
  7401. let dataPerson = self.getDataProfile(f_pin: dataMessages[indexPath.row]["f_pin"] as? String ?? "", message_id: dataMessages[indexPath.row]["message_id"] as? String ?? "")
  7402. titleReply.text = dataPerson["name"]
  7403. } else {
  7404. titleReply.text = "Bot"
  7405. }
  7406. }
  7407. titleReply.textColor = .orangeColor
  7408. let contentReply = UILabel()
  7409. self.containerPreviewReply.addSubview(contentReply)
  7410. contentReply.translatesAutoresizingMaskIntoConstraints = false
  7411. contentReply.leadingAnchor.constraint(equalTo: leftReply.leadingAnchor, constant: 10).isActive = true
  7412. contentReply.topAnchor.constraint(equalTo: titleReply.bottomAnchor).isActive = true
  7413. contentReply.trailingAnchor.constraint(equalTo: containerPreviewReply.trailingAnchor, constant: -20).isActive = true
  7414. contentReply.font = UIFont.systemFont(ofSize: 10 + offset())
  7415. let message_text = dataMessages[indexPath.row]["message_text"] as? String ?? ""
  7416. let attachment_flag = dataMessages[indexPath.row]["attachment_flag"] as? String ?? ""
  7417. let thumb_chat = dataMessages[indexPath.row]["thumb_id"] as? String ?? ""
  7418. let image_chat = dataMessages[indexPath.row]["image_id"] as? String ?? ""
  7419. let video_chat = dataMessages[indexPath.row]["video_id"] as? String ?? ""
  7420. let file_chat = dataMessages[indexPath.row]["file_id"] as? String ?? ""
  7421. if (attachment_flag == "0" && thumb_chat == "") {
  7422. contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  7423. } else if (attachment_flag == "1" || image_chat != "") {
  7424. if (message_text.trimmingCharacters(in: .whitespacesAndNewlines) == "") {
  7425. contentReply.text = "📷 Photo".localized()
  7426. } else {
  7427. contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  7428. }
  7429. } else if (attachment_flag == "2" || video_chat != "") {
  7430. if (message_text.trimmingCharacters(in: .whitespacesAndNewlines) == "") {
  7431. contentReply.text = "📹 Video".localized()
  7432. } else {
  7433. contentReply.attributedText = message_text.richText(group_id: self.dataGroup["group_id"] as? String ?? "")
  7434. }
  7435. } else if (attachment_flag == "6" || file_chat != ""){
  7436. contentReply.text = "📄 \(message_text.components(separatedBy: "|")[0])"
  7437. } else if (attachment_flag == "11") {
  7438. contentReply.text = "❤️ Sticker"
  7439. } else if attachment_flag == "27" {
  7440. contentReply.text = "📄 " + "Live Streaming".localized()
  7441. } else if attachment_flag == "26" {
  7442. contentReply.text = "📄 " + "Seminar".localized()
  7443. }
  7444. contentReply.textColor = .gray
  7445. let buttonCancelReply = UIButton(type: .custom)
  7446. self.containerPreviewReply.addSubview(buttonCancelReply)
  7447. buttonCancelReply.translatesAutoresizingMaskIntoConstraints = false
  7448. buttonCancelReply.trailingAnchor.constraint(equalTo: self.containerPreviewReply.trailingAnchor, constant: -10).isActive = true
  7449. buttonCancelReply.centerYAnchor.constraint(equalTo: self.containerPreviewReply.centerYAnchor).isActive = true
  7450. buttonCancelReply.setImage(UIImage(systemName: "xmark.circle" , withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular, scale: .default)), for: .normal)
  7451. buttonCancelReply.addTarget(nil, action: #selector(self.deleteReplyView), for: .touchUpInside)
  7452. buttonCancelReply.backgroundColor = .clear
  7453. buttonCancelReply.tintColor = .mainColor
  7454. if (attachment_flag == "1" || attachment_flag == "2" || image_chat != "" || video_chat != "") {
  7455. let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
  7456. let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
  7457. let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
  7458. if let dirPath = paths.first {
  7459. let thumbURL = URL(fileURLWithPath: dirPath).appendingPathComponent(thumb_chat)
  7460. let image : UIImage? = {
  7461. if let img = Nexilis.imageCache.object(forKey: thumb_chat as NSString) {
  7462. return img
  7463. }
  7464. else if let img = UIImage(contentsOfFile: thumbURL.path)?.resize(target: CGSize(width: 500, height: 500)) {
  7465. Nexilis.imageCache.setObject(img, forKey: thumb_chat as NSString)
  7466. return img
  7467. }
  7468. return nil
  7469. }()
  7470. // let image = UIGraphicsRenderer.renderImageAt(url: thumbURL as NSURL, size: CGSize(width: 250, height: 250))
  7471. let imageThumb = UIImageView(image: image)
  7472. self.containerPreviewReply.addSubview(imageThumb)
  7473. imageThumb.layer.cornerRadius = 2.0
  7474. imageThumb.clipsToBounds = true
  7475. imageThumb.translatesAutoresizingMaskIntoConstraints = false
  7476. imageThumb.trailingAnchor.constraint(equalTo: buttonCancelReply.leadingAnchor, constant: -10).isActive = true
  7477. imageThumb.centerYAnchor.constraint(equalTo: self.containerPreviewReply.centerYAnchor).isActive = true
  7478. imageThumb.widthAnchor.constraint(equalToConstant: 30).isActive = true
  7479. imageThumb.heightAnchor.constraint(equalToConstant: 30).isActive = true
  7480. if (attachment_flag == "2") {
  7481. let imagePlay = UIImageView(image: UIImage(systemName: "play.circle.fill"))
  7482. imageThumb.addSubview(imagePlay)
  7483. imagePlay.clipsToBounds = true
  7484. imagePlay.translatesAutoresizingMaskIntoConstraints = false
  7485. imagePlay.centerYAnchor.constraint(equalTo: imageThumb.centerYAnchor).isActive = true
  7486. imagePlay.centerXAnchor.constraint(equalTo: imageThumb.centerXAnchor).isActive = true
  7487. imagePlay.widthAnchor.constraint(equalToConstant: 10).isActive = true
  7488. imagePlay.heightAnchor.constraint(equalToConstant: 10).isActive = true
  7489. imagePlay.tintColor = .white
  7490. }
  7491. }
  7492. }
  7493. if (attachment_flag == "11") {
  7494. let imageSticker = UIImageView(image: UIImage(named: (message_text.components(separatedBy: "/")[1]), in: Bundle.resourceBundle(for: Nexilis.self), with: nil))
  7495. self.containerPreviewReply.addSubview(imageSticker)
  7496. imageSticker.layer.cornerRadius = 2.0
  7497. imageSticker.clipsToBounds = true
  7498. imageSticker.translatesAutoresizingMaskIntoConstraints = false
  7499. imageSticker.trailingAnchor.constraint(equalTo: buttonCancelReply.leadingAnchor, constant: -10).isActive = true
  7500. imageSticker.centerYAnchor.constraint(equalTo: self.containerPreviewReply.centerYAnchor).isActive = true
  7501. imageSticker.widthAnchor.constraint(equalToConstant: 30).isActive = true
  7502. imageSticker.heightAnchor.constraint(equalToConstant: 30).isActive = true
  7503. }
  7504. }
  7505. func scrollToFirstSearchMessage(indexScroll: Int = 1) {
  7506. if textSearch.count < 2 {
  7507. return
  7508. }
  7509. var lastIndex = 0
  7510. let messageTextForSearch: [[String: Any?]] = self.dataMessages.reversed()
  7511. for idx in 0..<messageTextForSearch.count {
  7512. if (messageTextForSearch[idx]["message_text"] as? String ?? "").lowercased().contains(textSearch) {
  7513. lastIndex += 1
  7514. if lastIndex < indexScroll {
  7515. continue
  7516. }
  7517. lastScrollIdxSearch = lastIndex
  7518. let section = self.dataDates.firstIndex(of: messageTextForSearch[idx]["chat_date"] as? String ?? "")
  7519. if section == nil {
  7520. return
  7521. }
  7522. let row = self.dataMessages.filter({ $0["chat_date"] as? String ?? "" == self.dataDates[section!]}).firstIndex(where: { $0["message_id"] as? String ?? "" == messageTextForSearch[idx]["message_id"] as? String ?? ""})
  7523. if row == nil {
  7524. return
  7525. }
  7526. let indexPath = IndexPath(row: row!, section: section!)
  7527. self.tableChatView.scrollToRow(at: indexPath, at: .middle, animated: true)
  7528. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
  7529. if let cell = self.tableChatView.cellForRow(at: indexPath) {
  7530. let containerMessage = cell.contentView.subviews[1]
  7531. let idMe = User.getMyPin() as String?
  7532. if (messageTextForSearch[idx]["f_pin"] as? String == idMe) {
  7533. containerMessage.backgroundColor = .blueBubbleColor.withAlphaComponent(0.3)
  7534. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  7535. if (messageTextForSearch[idx]["attachment_flag"] as? String == "11") {
  7536. containerMessage.backgroundColor = .clear
  7537. } else {
  7538. containerMessage.backgroundColor = .blueBubbleColor
  7539. }
  7540. }
  7541. } else {
  7542. containerMessage.backgroundColor = .whiteBubbleColor.withAlphaComponent(0.3)
  7543. DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  7544. if (messageTextForSearch[idx]["attachment_flag"] as? String == "11") {
  7545. containerMessage.backgroundColor = .clear
  7546. } else {
  7547. containerMessage.backgroundColor = .whiteBubbleColor
  7548. }
  7549. }
  7550. }
  7551. }
  7552. }
  7553. titleSearchMatches.isHidden = false
  7554. if countMatchesSearch != 0 {
  7555. if countMatchesSearch > 1 {
  7556. titleSearchMatches.text = "\(lastScrollIdxSearch) " + "of".localized() + " \(countMatchesSearch) " + "matches".localized()
  7557. } else {
  7558. titleSearchMatches.text = "\(countMatchesSearch) " + "matches".localized()
  7559. }
  7560. } else {
  7561. titleSearchMatches.text = "Not found".localized()
  7562. }
  7563. if lastScrollIdxSearch == countMatchesSearch || countMatchesSearch == 0 {
  7564. buttonUp.isEnabled = false
  7565. buttonUp.tintColor = .gray
  7566. } else {
  7567. buttonUp.isEnabled = true
  7568. buttonUp.tintColor = .mainColor
  7569. }
  7570. if countMatchesSearch == 0 || lastScrollIdxSearch == 1 || countMatchesSearch == 1 {
  7571. buttonDown.isEnabled = false
  7572. buttonDown.tintColor = .gray
  7573. } else {
  7574. buttonDown.isEnabled = true
  7575. buttonDown.tintColor = .mainColor
  7576. }
  7577. break
  7578. }
  7579. }
  7580. }
  7581. public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  7582. let indexPath = tableChatView.indexPathsForVisibleRows?.first
  7583. if indexPath != nil {
  7584. let headerRect = tableChatView.rectForHeader(inSection: indexPath!.section)
  7585. let isPinned = headerRect.origin.y <= scrollView.contentOffset.y
  7586. if listViewOnSection.count != 0 && listViewOnSection.count - 1 == indexPath!.section && indexPath!.row > 0 {
  7587. let sect = listViewOnSection.count - 1 < currentIndexpath!.section ? listViewOnSection.count - 1 : currentIndexpath!.section
  7588. let headerView = listViewOnSection[sect]
  7589. headerView.isHidden = true
  7590. }
  7591. }
  7592. }
  7593. public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  7594. if !decelerate {
  7595. let indexPath = tableChatView.indexPathsForVisibleRows?.first
  7596. if indexPath != nil {
  7597. let headerRect = tableChatView.rectForHeader(inSection: indexPath!.section)
  7598. let isPinned = headerRect.origin.y <= scrollView.contentOffset.y
  7599. if listViewOnSection.count != 0 && listViewOnSection.count - 1 == indexPath!.section && isPinned {
  7600. let sect = listViewOnSection.count - 1 < currentIndexpath!.section ? listViewOnSection.count - 1 : currentIndexpath!.section
  7601. let headerView = listViewOnSection[sect]
  7602. headerView.isHidden = true
  7603. }
  7604. }
  7605. }
  7606. }
  7607. }
  7608. extension EditorGroup: UISearchBarDelegate {
  7609. public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  7610. timerSearch?.invalidate()
  7611. if searchText.count > 1 {
  7612. timerSearch = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: {[self] _ in
  7613. textSearch = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
  7614. titleSearchMatches.isHidden = true
  7615. countMatchesSearch = Chat.getCountSearchMessage(key: textSearch, pin: (self.dataGroup["group_id"] as? String) ?? "", chatId: (self.dataTopic["chat_id"] as? String) ?? "", isPersonal: false)
  7616. tableChatView.reloadData()
  7617. scrollToFirstSearchMessage()
  7618. })
  7619. }
  7620. }
  7621. }